Swift5.1.3 Xcode11.3 SwiftUI
- カラーで色分けするカレンダーアプリの作り方
- 指定した予定日の前後で色を変更する
- Dateをif文で検証してColorの変数を調整します
「以下は動画のハイライトです。」
第3者が作成したカレンダーアプリなどはプライベートな予定がインターネットを介してどこにどう流れるかわかりません。他の端末と同期したくないのであればアプリを自分で作ってしまいましょう。
また、自作アプリであれば同期なしでいろいろカスタマイズすることも可能です。
本稿ではカレンダーを予定日を基準にして色分けするアプリの作り方を紹介させていただきます。
日付を扱う変数はSwiftでいろいろとデフォルトで用意されております。例えば以下のfunctionでは日付の情報からString型で文字表示できるように変換してくれます。
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のアルファベットの文字はそれぞれ月、日、年を表すもので例えばMをMMなどにすると二桁表示になったりします。ちなみにMMMMと入力すると月の名前を表示してくれたりといろいろ便利です。
こういったDateのカレンダー機能をフル活用して以下の二つのStruct (CLCellと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)
}
}
この中で重要な箇所はCLCellの中の以下です。
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)
}
この中の.foregroundColor(clDate.getColor())がカレンダーを色分けするために必要なところです。引数に別のstruct
CLDateをパラメーターに持ちfunc getColor()でif文を用いて現在の前後を評価して色分けを行なっております。
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
}
カレンダーを作成するコードはちょっと難しいですが基本以下をコピペでOKです。
他に3つのstructと1つのclassを作成する必要があります。
まず以下はカレンダーの上限と下限を決定します
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)!
}
}
カレンダーのデータ管理は以下で行います。
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) })
}
}
一週間ごとにセルを調整したりカレンダーらしいデザインにするために以下を用います。
ちょっとfunctionが多いですがもしかしたら不要なものもあるかもしれません。適宜追加削除を行なって自分流に書き換えてください。
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)
}
これをContentView内に以下のように入れて作成してみてください。
初心者の方はとりあえず最初の方の色分けする機能あたりをいじくって遊んでみると良いと思います。
@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)
}
ソースコードはYouTubeのコメント欄に記載します。
目次へ戻る