implement Task planner App

Pub Date: 2023-07-30

Drag fons into Bundle Resources

image-20230730195636561

Add font key to Info.plist

image-20230730201103891

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>ATSApplicationFontsPath</key>
	<string></string>
	<key>UIAppFonts</key>
	<array>
		<string>ubuntu.bold.ttf</string>
		<string>ubuntu.medium.ttf</string>
		<string>ubuntu.light.ttf</string>
		<string>ubuntu.regular.ttf</string>
	</array>
</dict>
</plist>

Model/Ubuntu.swift

import SwiftUI

/// MARK :custom font extension
enum Ubuntu{
    case light
    case bold
    case medium
    case regular

    var weight:Font.Weight{
        switch self{
        case .light:
            return .light
        case .bold:
            return .bold
        case .medium:
            return .medium
        case .regular:
            return .regular
        }
    }
}


extension View{
    func Ubuntu(_ size:CGFloat,_ weight:Ubuntu)->some View{
        self.font(.custom("ubuntu", size: size))
            .fontWeight(weight.weight)
    }
}

Model/Task.swift


import SwiftUI

//MARK:Task model
struct Task:Identifiable{
    var id:UUID = UUID()
    var dateadded:Date
    var taskName:String
    var taskDescription:String
    var taskCategory:Category
}
enum Category:String, CaseIterable{
    case bug="bug"
    case general="general"
    case idea="idea"
    case challenge="challenge"
    case coding="coding"
    case modifiers="modifiers"
 
    
    var color:Color{
        switch self{
        case .bug:
            return Color.green
        case .general:
            return Color.gray
        case .idea:
            return Color.pink
        case .challenge:
            return Color.purple
        case .coding:
            return Color.brown
        case .modifiers:
            return Color.cyan
        }
    }
}


var sampleTasks:[Task] = [
    .init(dateadded: Date(timeIntervalSince1970:1672948935), taskName: "Learn Swift", taskDescription: "", taskCategory: .coding),
    .init(dateadded: Date(timeIntervalSince1970:1672946935), taskName: "Learn Swift", taskDescription: "", taskCategory: .coding),
    .init(dateadded: Date(timeIntervalSince1970:1672946935), taskName: "Learn Swift", taskDescription: "", taskCategory: .coding),
    .init(dateadded: Date(timeIntervalSince1970:1672936935), taskName: "Learn Swift", taskDescription: "", taskCategory: .coding),
    .init(dateadded: Date(timeIntervalSince1970:1672946935), taskName: "Learn Swift", taskDescription: "", taskCategory: .coding)
]

View/Home.swift


import SwiftUI

struct Home: View {
    /// View propertyes
    @State private var currentDay:Date = .init()
    @State private var tasks:[Task] = sampleTasks
    @State private var addNewTask:Bool = false
    var body: some View {
        ScrollView(.vertical,showsIndicators: false){
            TimelineView().padding(15)
        }.safeAreaInset(edge: .top,spacing: 0){
            HeaderView()
        }.fullScreenCover(isPresented: $addNewTask){
            AddNewView{task in
                //simply add to tasks
                tasks.append(task)
            }
        }
    }
    /// - Time Line View
    @ViewBuilder
    func TimelineView()->some View{
        VStack{
            let hovers = Calendar.current.hovers
            ForEach(hovers,id:\.self){hover in
                TimelineViewRow(hover)
            }
        }
        
    }
    @ViewBuilder
    func TimelineViewRow(_ date:Date)->some View{
        HStack(alignment: .top){
            Text(date.toString("h a")).Ubuntu(14, .regular).frame(width: 45,alignment: .leading)
            
            ///Filtring taks
            let calendar = Calendar.current
            let filteredTasks =  tasks.filter{
                if let hour = calendar.dateComponents([.hour], from:date ).hour,
                    let taskHour = calendar.dateComponents([.hour], from: $0.dateadded).hour,
                    hour==taskHour && calendar.isDate($0.dateadded, inSameDayAs: currentDay){
                        return true
                    }
                return false
            }
            if filteredTasks.isEmpty{
                Rectangle().stroke(.gray.opacity(0.5),style: StrokeStyle(lineWidth: 0.5,lineCap: .butt,lineJoin: .bevel,dash: [5],dashPhase: 5))
                    .frame(height: 0.5)
                    .offset(y:10)
            }else{
                VStack(spacing: 10){
                    ForEach(filteredTasks){task in
                        TaskRow(task)
                        
                    }
                }
            }
            
        }.hAlign(.leading)
        .padding(.vertical,15)
    }
    @ViewBuilder
    func TaskRow(_ task:Task)->some View{
        VStack(alignment: .leading,spacing: 8){
            Text(task.taskName.uppercased()).Ubuntu(16, .regular).foregroundColor(task.taskCategory.color)
            if task.taskDescription != ""{
                Text(task.taskDescription).Ubuntu(14, .light)
                    .foregroundColor(task.taskCategory.color.opacity(0.8))
            }
        }.hAlign(.leading)
            .padding(12)
            .background{
                ZStack(alignment: .leading){
                    Rectangle().fill(task.taskCategory.color).frame(width: 4)
                    Rectangle().fill(task.taskCategory.color.opacity(0.25 ))
                }
            }
        
    }
    /// - Header View
    @ViewBuilder
    func HeaderView()->some View{
        VStack{
            HStack{
                VStack(alignment: .leading,spacing: 6){
                    Text("今天").Ubuntu(30, .regular)
                    
                    Text("欢迎使用日历计划App").Ubuntu(14, .light)
                }.hAlign(.leading)
                Button{
                    addNewTask.toggle()
                }label: {
                    HStack(spacing: 10){
                        Image(systemName: "plus")
                        Text("添加任务").Ubuntu(15, .regular)
                    }
                }.padding(.vertical,10).padding(.horizontal,15).background{
                    Capsule().fill(Color.blue.gradient)
                }.foregroundColor(.white)
            }
            
            /// Today date in string
            Text(Date().toString("MMM YYYY")).Ubuntu(16, .medium).hAlign(.leading)
                .padding(.top,15)
            
            
            //Week row
            WeekRowView()
            
        }.padding(15)
            .background{
                VStack(spacing: 0){
                    Color.white
                    Rectangle().fill(.linearGradient(colors: [.white,.clear], startPoint: .top, endPoint: .bottom))
                        .frame(height: 20)
                }.ignoresSafeArea()
            }
    }
    /// - Week Row View
    @ViewBuilder
    func WeekRowView()->some View{
        HStack(spacing: 0){
            ForEach(Calendar.current.currentWeek){ weekDay in
                let staus = Calendar.current.isDate(weekDay.date, inSameDayAs: currentDay)
                VStack{
                    Text(weekDay.string.prefix(3))
                        .Ubuntu(13, .medium)
                    Text(weekDay.date.toString("dd"))
                        .Ubuntu(16, .regular)
                }
                .foregroundColor(staus ? Color.blue:Color.gray)
                .hAlign(.center)
                .contentShape(Rectangle())
                .onTapGesture {
                    withAnimation(.easeInOut(duration: 0.3)){
                        currentDay = weekDay.date
                    }
                }
            }
        }.padding(.horizontal,-15)
    }
    
 
}

struct Home_Previews: PreviewProvider {
    static var previews: some View {
        Home()
    }
}

///MARK:View extensions
extension View{
    func hAlign(_ alignment:Alignment)->some View{
        self.frame(maxWidth: .infinity,alignment: alignment)
    }
    func vAlign(_ alignment:Alignment)->some View{
        self.frame(maxHeight: .infinity,alignment: alignment)
    }
}


///MARK:Date extension
extension Date{
    func toString(_ format:String)->String{
        let formatter = DateFormatter()
        formatter.dateFormat = format
        return formatter.string(from: self)
    }
   
}
///MARK:Calendar extension
extension Calendar{
    ///return 24 hover in a day
    var hovers:[Date]{
        let startOfDay = self.startOfDay(for: Date())
        var hovers:[Date] = []
        for index in 0..<24{
            if let date = self.date(byAdding: .hour, value: index, to: startOfDay){
                hovers.append(date)
            }
        }
        return hovers
    }
    
    /// return current week in Array format
    var currentWeek:[WeekDay]{
        guard let firstWeekDay = self.dateInterval(of: .weekOfMonth, for: Date())?.start else{
            return []
        }
        var week:[WeekDay] = []
        for index in 0..<7{
            if let day = self.date(byAdding: .day, value: index, to: firstWeekDay){
                let weekDaySymbol:String = day.toString("EEEE")
                let isToday = self.isDateInToday(day)
                week.append(.init(string: weekDaySymbol, date: day,isToday: isToday))
            }
        }
        return week
    }
    struct WeekDay:Identifiable{
        var id:UUID = .init()
        var string:String
        var date:Date
        var isToday:Bool = false
    }
}

View/AddNewTaskView.swift


import SwiftUI

struct AddNewView: View {
    /// - Calback
    var onAdd: ((Task) -> ())

    /// -View properies
    @Environment(\.dismiss) private var dismiss
    @State private var taskName:String = ""
    @State private var taskDescription:String = ""
    @State private var taskDate:Date = .init()
    @State private var taskCategory:Category = .general
    //Category animation properties
    @State private var animatedColor:Color = Category.general.color
    @State private var animate:Bool = false
    var body: some View {
        VStack(alignment: .leading){
            VStack(alignment: .leading,spacing: 10){
                //back button
                Button(action: {
                    dismiss()
                }, label: {
                    Image(systemName: "chevron.left").foregroundColor(.white).contentShape(Rectangle())
                })
                //create new task
                Text("添加任务").Ubuntu(28, .light).foregroundColor(.white).padding(.vertical,15)
                TitleView("名字")
                
                TextField("备考雅思",text:$taskName).Ubuntu(16, .regular).tint(.white).padding(.top,2)
                
                Rectangle().frame(height: 1).foregroundColor(.white.opacity(0.7))
                
                TitleView("日期")
                
                HStack(alignment: .bottom,spacing: 12 ){
                    HStack(spacing: 12){
                        Text(taskDate.toString("EEEE dd,MMMM"))
                        
                        //Date picker
                        Image(systemName: "calendar").font(.title).foregroundColor(.white).overlay{
                            DatePicker("",selection: $taskDate,displayedComponents: [.date]).blendMode(.destinationOver)
                          }
                    }
                    .offset(y:-5)
                    .overlay(alignment: .bottom){
                        Rectangle().fill(.white.opacity(0.7)).frame(height: 1).offset(y:5)
                    }
                    
                    HStack(spacing: 12){
                        Text(taskDate.toString("hh:MM a"))
                        
                        //Date picker
                        Image(systemName: "clock").font(.title).foregroundColor(.white).overlay{
                            DatePicker("",selection: $taskDate,displayedComponents: [.hourAndMinute]).blendMode(.destinationOver)
                          }
                    }
                    .offset(y:-5)
                    .overlay(alignment: .bottom){
                        Rectangle().fill(.white.opacity(0.7)).frame(height: 1).offset(y:5)
                    }
                }.padding(.bottom,15)
                
            }
            .environment(\.colorScheme, .dark)
            .hAlign(.leading).padding(15).background{
                ZStack{
                    taskCategory.color.ignoresSafeArea()
                    
                    GeometryReader{
                        let size = $0.size
                        Rectangle().fill(animatedColor).mask{
                            Circle()
                        }.frame(width: animate ? size.width*2:0,height: animate ? size.height*2:0).offset(animate ? CGSize(width: -size.width/2, height: -size.height/2):size)
                    }.clipped()
                }
                
              
            }
            
            //discription
            VStack(alignment: .leading,spacing: 10){
                TitleView("描述",.gray)
                
                TextField("关于任务描述",text:$taskDescription).Ubuntu(16, .regular).tint(.white).padding(.top,2)
                
                Rectangle().fill(.black.opacity(0.2)).frame(height: 1)
                
                TitleView("分类",.gray).padding(.top,15)
                
                LazyVGrid(columns: Array(repeating: .init(.flexible(),spacing: 20),count: 3),spacing: 15){
                    ForEach(Category.allCases,id:\.rawValue){category in
                        Text(category.rawValue.uppercased()).Ubuntu(12, .regular).padding(.vertical,5).background{
                            RoundedRectangle(cornerRadius: 5,style:.continuous).fill(category.color.opacity(0.3)).frame(width: 85)
                        }.foregroundColor(category.color).contentShape(Rectangle()).onTapGesture {
                            guard !animate else{
                                return
                            }
                            animatedColor = category.color
                            withAnimation(.interactiveSpring(response: 0.7,dampingFraction: 1,blendDuration: 1)){
                                animate = true
                            }
                            DispatchQueue.main.asyncAfter(deadline: .now()+0.7){
                                animate = false
                                taskCategory = category
                            }
                        }
                    }
                }.padding(.top,5)
                
                Button{
                    //get all the Task date and pass it to callback
                    let task  = Task(dateadded: taskDate, taskName: taskName, taskDescription: taskDescription, taskCategory: taskCategory)
                    (onAdd(task))
                    dismiss()
                } label: {
                    Text("添加任务").Ubuntu(16, .regular).foregroundColor(.white).padding(.vertical,15).hAlign(.center).background{
                        Capsule().fill(animatedColor.gradient)
                    }
                }.vAlign(.bottom).disabled(taskName == ""||animate).opacity(taskName == "" ? 0.6:1)
            }.padding(15)
        }.vAlign(.top)
    }
    
    @ViewBuilder
    func TitleView(_ value:String,_ color:Color = .white.opacity(0.7))->some View{
        Text(value).Ubuntu(16, .regular)
            .foregroundColor(color)
    }
}

struct AddNewView_Previews: PreviewProvider {
    static var previews: some View {
        AddNewView{task in
            
        }
    }
}

Simulator Screenshot - iPhone 14 Pro - 2023-07-30 at 21.34.44

Simulator Screenshot - iPhone 14 Pro - 2023-07-30 at 21.35.49