[SwiftUI] 커스텀 캘린더 SwiftData와 연동하여 기능 추가 하기

이상현·2024년 4월 17일

Swift

목록 보기
6/10
post-thumbnail

Tistory iOYES - SwiftUI로 캘린더 직접 구현하기 게시글의 커스텀 캘린더의 코드를 사용하여 기능을 추가 하는 게시글입니다.

이 게시글은 위 블로그에서 구현한 캘린더 = 커스텀 캘린더 에 다음 기능을 추가하는 과정을 설명합니다.

  1. 오늘 날짜 표시
  2. SwiftData 를 사용하여 일정이 있는 날 캘린더에 표시

0. 기본 세팅

1. 해당 블로그에서 커스텀 캘린더 코드를 xcode로 가져온다.

2. 커스텀 캘린더CalenderView 오타를 고친다.

CalenderView 우클릭 -> refactor -> rename -> CalendarView

3. CellView.scaledToFit 추가

CellView가 날짜 한 칸의 뷰이다.

private struct CellView: View {
    var day: Int
    var clicked: Bool = false
    
    init(day: Int, clicked: Bool) {
        self.day = day
        self.clicked = clicked
    }
    
    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 5)
                .opacity(0)
                .overlay(Text(String(day)))
                .foregroundColor(.blue)
            
            if clicked {
                Text("Click")
                    .font(.caption)
                    .foregroundColor(.red)
            }
        }
        .scaledToFit()
    }
}

1. 오늘 날짜 표시

1. CellViewcellDate: Date 속성 추가하기

private struct CellView: View {
    var day: Int
    var clicked: Bool = false
    var cellDate: Date
    
    init(day: Int, clicked: Bool, cellDate: Date) {
        self.day = day
        self.clicked = clicked
        self.cellDate = cellDate
    }
    
    // body 생략
}

2. calendarGridView 안에 CellView 호출 하는 부분에서 cellDate: date 인자 추가하기

CellView(day: day, clicked: clicked, cellDate: date)

3. 날짜 비교를 위한 extension Date 함수 추가하기

extension Date {
    private func startOfDay() -> Date {
        Calendar.current.startOfDay(for: self)
    }
    
    func isSameDate(date: Date) -> Bool {
        self.startOfDay() == date.startOfDay()
    }
}

4. Cellviewbody 에 로직에 따라 뷰가 바뀌도록 if문과 함수 추가

private struct CellView: View {
	// 프로퍼티 및 생성자 생략
  
	var body: some View {
		ZStack {
			// Date() 는 현재 시간의 Date를 반환한다.
			// cellDate 와 Date() 이 같은날이면 Circle 표시
			if cellDate.isSameDate(date: Date()) {
              Circle()
			}
            RoundedRectangle(cornerRadius: 5)
                .opacity(0)
                .overlay(Text(String(day)))
                .foregroundColor(.blue)
            // clicked 로직 삭제함
        }
        .scaledToFit()
    }
}

오늘 날짜 표시 성공

2. SwiftData 를 사용하여 일정이 있는 날 캘린더에 표시

1. 데이터 모델 만들기 (예시)

// MyModel.swift
import SwiftUI
import SwiftData

@Model
class MyModel {
    var date: Date
    var content: String
    
    init(date: Date, content: String) {
        self.date = date
        self.content = content
    }
}

2. 앱 시작 지점 파일에서 modelContainer 넣어주기

// MyCalendarApp.swift
import SwiftUI
import SwiftData

@main
struct MyCalendarApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(models: sampleData)
                .modelContainer(for: MyModel.self)
        }
    }
}

3. 최상단 뷰 ContentView 의 프로퍼티에 @query 프로퍼티 래퍼로 모델 배열 만들기

//ContentView.swift
import SwiftUI
import SwiftData

struct ContentView: View {
    @Query var myModels: [MyModel] // @Query 로 모델 배열 넣음
    
    var body: some View {
        CalendarView(month: Date())
            .padding()
            
        // 기타 다른 뷰
        RoundedRectangle(cornerRadius: 25.0)
        	.fill(Color.gray)
    }
}

3-1. 프리뷰에 임시 데이터 넣기 (선택)

// SampleDate.swift
import SwiftData
import SwiftUI

@MainActor
let sampleData: ModelContainer = {
    do {
        let container = try ModelContainer(
            for: MyModel.self,
            configurations: ModelConfiguration(isStoredInMemoryOnly: true)
        )
        let modelContext = container.mainContext
        if try modelContext.fetch(FetchDescriptor<MyModel>()).isEmpty {
            mockData.forEach { container.mainContext.insert($0) }
        }
        return container
    } catch {
        fatalError("Failed to create container")
    }
}()

var mockData = [
    MyModel(date: Date().addingTimeInterval(-1233), content: "아침에 달리기 좋았음"),
    MyModel(date: Date().addingTimeInterval(-252200), content: "오늘은 좀 피곤했음"),
    MyModel(date: Date().addingTimeInterval(-482000), content: "괜찮은 운동이었음"),
    MyModel(date: Date().addingTimeInterval(-614800), content: "끝까지 하기 힘들었음"),
    MyModel(date: Date().addingTimeInterval(-834000), content: "평범한 하루"),
    MyModel(date: Date().addingTimeInterval(-1396000), content: "개인 기록 경신"),
    MyModel(date: Date().addingTimeInterval(-1792730), content: "일정한 속도 유지")
]

위와 같이 작성한 후

프리뷰를 다음과 같이 작성하면 modelContainer에 임시 데이터를 넣어서 프리뷰 할 수 있다.

#Preview {
    ContentView()
        .modelContainer(sampleData)
}

4. 모델 데이터를 하위 뷰로 이동시키기

다음과 같이 상위 뷰에서 모델 배열 데이터가 CellView 까지 이동한다.

@Query 를 프로퍼티 래퍼로 붙혀준 프로퍼티는 값이 변경될 때 마다 연관된 뷰가 자동으로 업데이트 된다.

그러므로 클래스의 인스턴스의 배열인 myModels: [MyModel] 은 그냥 하위 뷰로 전달만 해주면 변경될때 마다 뷰가 자동으로 업데이트 된다.

  • 우선 CalendarViewmyModels 를 추가한다.
// ContentView 에서의 호출부
CalendarView(month: Date(), myModels: myModels)
struct CalendarView: View {
    @State var month: Date
    @State var offset: CGSize = CGSize()
    @State var clickedDates: Set<Date> = []
    var myModels: [MyModel]
    
    // 이하 생략
  • 이후 myModels 에서 셀의 날짜에 알맞은 날짜의 데이터를 가져오는 함수 작성
/// myModels 에서 otherDate 와 동일한 날인 데이터 하나 찾아서 반환. 없으면 nil 반환
func myModelByDate(otherDate: Date) -> MyModel? {
    return myModels.first { myModel in
        startOfDay(date: myModel.date) == startOfDay(date: otherDate)
    }
}

/// Date 를 해당 날짜의 자정으로 바꿔줌 (시 분 초기화)
func startOfDay(date: Date) -> Date {
    Calendar.current.startOfDay(for: date)
}
  • calendarGridView 내부 Cellview 호출 부분 위에 함수 사용하여 날짜에 맞는 모델 저장,
  • Cellviewvar myModel: MyModel? 프로퍼티 추가 및 호출부 수정
  • 뷰 부분에 myModel?nil 이 아닐 경우 빨간색 원 표시되도록 추가
//..
let myModel = myModelByDate(otherDate: date)
CellView(day: day, clicked: clicked, cellDate: date, myModel: myModel)
//..
private struct CellView: View {
    var day: Int
    var clicked: Bool = false
    var cellDate: Date
    var myModel: MyModel?
    
    init(day: Int, clicked: Bool, cellDate: Date, myModel: MyModel?) {
        self.day = day
        self.clicked = clicked
        self.cellDate = cellDate
        self.myModel = myModel
    }
    
    var body: some View {
        ZStack {
            if let myModel {
            	// myModel 에 데이터가 있을 경우 표시될 뷰
                Circle()
                    .fill(Color.red)
            }
            if cellDate.isSameDate(date: Date()) {
                Circle()
            }
            RoundedRectangle(cornerRadius: 5)
                .opacity(0)
                .overlay(Text(String(day)))
                .foregroundColor(.blue)
        }
        .scaledToFit()
    }
}

일정 있는 날짜 표시 성공

0개의 댓글