SwiftUI - Color the day of event with colorful custom calendar you develop | Apple iOS Xcode YouTube Seminar #12


Swift5.1.3 Xcode11.3 SwiftUI


- Change colors in specific days of event in calendar
- Settings for the range of days in calendar function
- Programmatically create colorful calendar with if-statement


Label some events with colorful custom colors in iOS calendar you create


* Here is the highlight of YouTube tutorial.

Are you anxious about managing your schedule with the calendar app a third party creates? Unexpected syncing on cloud may share your private information with someone else.

In this aspect, I would recommend you to know how to create iOS app. In addition, you can customize the design of calendar as you want.

In this tutorial, let me make changing event color in the calendar as one of the examples. Xcode already provides various functions. The date setting is also unique like this. This will enable date information to be shown in String format.


func getTextFromDate(date: Date!) -> String {
let formatter = DateFormatter()
formatter.locale = .current
formatter.dateFormat = "M-d-yyyy"
return date == nil ? "" : formatter.string(from: date)
}



"M-d-yyyy" determine the design of letters. Try to change the part, "M" to "MM" and you can get Month information in two digits. If you make it to "MMMM", the name of Month will be shown. It is very convenient.

Now, let's take a look at settings for the design of calendar. At first, I will use the following two structs named as CLCell and CLDate.


struct CLCell: View {

var clDate: CLDate

var cellWidth: CGFloat

var body: some View {
Text(clDate.getText())
.fontWeight(clDate.getFontWeight())
.foregroundColor(clDate.getColor())
.frame(width: cellWidth, height: cellWidth)
.font(.system(size: 20))
.cornerRadius(cellWidth/2)
}
}

struct CLDate {

var date: Date
let clManager: CLManager

var isToday: Bool = false
var isSelected: Bool = false

init(date: Date, clManager: CLManager, isToday: Bool, isSelected: Bool) {

self.date = date
self.clManager = clManager
self.isToday = isToday
self.isSelected = isSelected
}

func getText() -> String {
let day = formatDate(date: date, calendar: self.clManager.calendar)
return day
}


func getFontWeight() -> Font.Weight {
var fontWeight = Font.Weight.medium

if isSelected {
fontWeight = Font.Weight.heavy
} else if isToday {
fontWeight = Font.Weight.heavy
}
return fontWeight
}

func getColor() -> Color? {
var col = Color.black

if isSelected {
col = Color.red
} else if isToday {
col = Color.black
} else if date < Date() {
col = Color.blue
} else if date > Date() {
col = Color.yellow
}
return col

}

func formatDate(date: Date, calendar: Calendar) -> String {
let formatter = dateFormatter()
return stringFrom(date: date, formatter: formatter, calendar: calendar)
}

func dateFormatter() -> DateFormatter {
let formatter = DateFormatter()
formatter.locale = .current
formatter.dateFormat = "d"
return formatter
}

func stringFrom(date: Date, formatter: DateFormatter, calendar: Calendar) -> String {
if formatter.calendar != calendar {
formatter.calendar = calendar
}
return formatter.string(from: date)
}
}



In these parts, the following code take the role of design for the calendar.


var body: some View {
Text(clDate.getText())
.fontWeight(clDate.getFontWeight())
.foregroundColor(clDate.getColor())
.frame(width: cellWidth, height: cellWidth)
.font(.system(size: 20))
.cornerRadius(cellWidth/2)
}



You can change the color of the calendar with foregroundColor(clDate.getColor()). In its parameter, it has the other struct CLDate. Let's see the function, getColor() in it. The color code is adjusted by if-statement.


func getColor() -> Color? {
var col = Color.black

if isSelected {
col = Color.red
} else if isToday {
col = Color.black
} else if date < Date() {
col = Color.blue
} else if date > Date() {
col = Color.yellow
}
return col
}



The rest codes are a little difficult and very long. So, if you are beginner, just copy and paste the following. There are 3 structs and 1 class to create the calendar function.

You can determine the minimum and maximum date of the calendar with this struct, CLViewController.


struct CLViewController: View {

@Binding var isPresented: Bool
@ObservedObject var clManager: CLManager

var body: some View {
Group {
List {
ForEach(0..<numberOfMonths()) { index in
CLMonth(isPresented: self.$isPresented, clManager: self.clManager, monthOffset: index)
}
Divider()
}
}
}

func numberOfMonths() -> Int {
return clManager.calendar.dateComponents([.month], from: clManager.minimumDate, to: CLMaximumDateMonthLastDay()).month! + 1
}

func CLMaximumDateMonthLastDay() -> Date {
var components = clManager.calendar.dateComponents([.year, .month, .day], from: clManager.maximumDate)
components.month! += 1
components.day = 0
return clManager.calendar.date(from: components)!
}
}



The following class CLManager manages the date information.


class CLManager : ObservableObject {

@Published var calendar = Calendar.current
@Published var minimumDate: Date = Date()
@Published var maximumDate: Date = Date()
@Published var selectedDates: [Date] = [Date]()
@Published var selectedDate: Date! = nil



init(calendar: Calendar, minimumDate: Date, maximumDate: Date, selectedDates: [Date] = [Date]()) {
self.calendar = calendar
self.minimumDate = minimumDate
self.maximumDate = maximumDate
self.selectedDates = selectedDates
}

func selectedDatesContains(date: Date) -> Bool {
if let _ = self.selectedDates.first(where: { calendar.isDate($0, inSameDayAs: date) }) {
return true
}
return false
}


func selectedDatesFindIndex(date: Date) -> Int? {
return self.selectedDates.firstIndex(where: { calendar.isDate($0, inSameDayAs: date) })
}

}


The cell of month can be ordered weekly by the following struct, CLMonth.



struct CLMonth: View {

@Binding var isPresented: Bool
@ObservedObject var clManager: CLManager

let monthOffset: Int
let calendarUnitYMD = Set<Calendar.Component>([.year, .month, .day])
let daysPerWeek = 7
var monthsArray: [[Date]] {
monthArray()
}
let cellWidth = CGFloat(32)

@State var showTime = false

var body: some View {
VStack(alignment: HorizontalAlignment.center, spacing: 10){
Text(getMonthHeader())
VStack(alignment: .leading, spacing: 5) {
ForEach(monthsArray, id: \.self) { row in
HStack() {
ForEach(row, id: \.self) { column in
HStack() {
Spacer()
if self.isThisMonth(date: column) {
CLCell(clDate: CLDate(
date: column,
clManager: self.clManager,
isToday: self.isToday(date: column),
isSelected: self.isSpecialDate(date: column)
),
cellWidth: self.cellWidth)
.onTapGesture { self.dateTapped(date: column) }
} else {
Text("").frame(width: self.cellWidth, height: self.cellWidth)
}
Spacer()
}
}
}
}
}
}
}

func isThisMonth(date: Date) -> Bool {
return self.clManager.calendar.isDate(date, equalTo: firstOfMonthForOffset(), toGranularity: .month)
}

func dateTapped(date: Date) {
if self.clManager.selectedDate != nil &&
self.clManager.calendar.isDate(self.clManager.selectedDate, inSameDayAs: date) {
self.clManager.selectedDate = nil
} else {
self.clManager.selectedDate = date
}
self.isPresented = false
}

func monthArray() -> [[Date]] {
var rowArray = [[Date]]()
for row in 0 ..< (numberOfDays(offset: monthOffset) / 7) {
var columnArray = [Date]()
for column in 0 ... 6 {
let abc = self.getDateAtIndex(index: (row * 7) + column)
columnArray.append(abc)
}
rowArray.append(columnArray)
}
return rowArray
}

func getMonthHeader() -> String {
let headerDateFormatter = DateFormatter()
headerDateFormatter.calendar = clManager.calendar
headerDateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "yyyy LLLL", options: 0, locale: clManager.calendar.locale)

return headerDateFormatter.string(from: firstOfMonthForOffset()).uppercased()
}

func getDateAtIndex(index: Int) -> Date {
let firstOfMonth = firstOfMonthForOffset()
let weekday = clManager.calendar.component(.weekday, from: firstOfMonth)
var startOffset = weekday - clManager.calendar.firstWeekday
startOffset += startOffset >= 0 ? 0 : daysPerWeek
var dateComponents = DateComponents()
dateComponents.day = index - startOffset

return clManager.calendar.date(byAdding: dateComponents, to: firstOfMonth)!
}

func numberOfDays(offset : Int) -> Int {
let firstOfMonth = firstOfMonthForOffset()
let rangeOfWeeks = clManager.calendar.range(of: .weekOfMonth, in: .month, for: firstOfMonth)

return (rangeOfWeeks?.count)! * daysPerWeek
}

func firstOfMonthForOffset() -> Date {
var offset = DateComponents()
offset.month = monthOffset

return clManager.calendar.date(byAdding: offset, to: CLFirstDateMonth())!
}

func CLFormatDate(date: Date) -> Date {
let components = clManager.calendar.dateComponents(calendarUnitYMD, from: date)

return clManager.calendar.date(from: components)!
}

func CLFormatAndCompareDate(date: Date, referenceDate: Date) -> Bool {
let refDate = CLFormatDate(date: referenceDate)
let clampedDate = CLFormatDate(date: date)
return refDate == clampedDate
}

func CLFirstDateMonth() -> Date {
var components = clManager.calendar.dateComponents(calendarUnitYMD, from: clManager.minimumDate)
components.day = 1

return clManager.calendar.date(from: components)!
}


func isToday(date: Date) -> Bool {
return CLFormatAndCompareDate(date: date, referenceDate: Date())
}

func isSpecialDate(date: Date) -> Bool {
return isSelectedDate(date: date)
}



func isSelectedDate(date: Date) -> Bool {
if clManager.selectedDate == nil {
return false
}
return CLFormatAndCompareDate(date: date, referenceDate: clManager.selectedDate)
}


Let's insert all the codes above into ContentView. Have fun with changing parameters one build by one.



@State var sheetPresented = false

var clManagerX = CLManager(calendar: Calendar.current, minimumDate: Date(), maximumDate: Date().addingTimeInterval(60*60*24*365))

var body: some View {
VStack (spacing: 15) {

Button(action: { self.sheetPresented.toggle() }) {
Text("Check Calendar").foregroundColor(.blue)
}
.font(.largeTitle)
.sheet(isPresented: self.$sheetPresented, content: {
CLViewController(isPresented: self.$sheetPresented, clManager: self.clManagerX)})
Text(self.getTextFromDate(date: self.clManagerX.selectedDate))

}
}

func getTextFromDate(date: Date!) -> String {
let formatter = DateFormatter()
formatter.locale = .current
formatter.dateFormat = "M-d-yyyy"
return date == nil ? "" : formatter.string(from: date)
}




To get the source code, check the comment of my YouTube

Back to Table List

2020年01月10日