Greetings, traveler!
We continue our series about SwiftUI Week Calendar View. This is the fifth part, where we will create the WeekCalendarView
. The title of this article looks a bit weird, but it marks that this article is a key one.
If you just want to check out the complete source code, here it is.
As we discussed, we should create a TabView
. It stores and regenerates three tabs whenever the parent view retrieves a new reference date value.
For data management, we will use the WeekProvider
we created earlier.
Add two additional properties. The first one is the scrollDirection
. This property will help the provider calculate new Week
models to display. The second activeTab
property will hold the active tab value. We will use it to switch tabs programmatically. Both of them will have a WeekPosition
type.
Also, remember to add the selectedDate
binding property.
struct WeekCalendarView: View {
@State private var provider: WeekProvider = .init()
@State private var activeTab: WeekPosition = .middle
@State private var scrollDirection: WeekPosition = .middle
@Binding var selectedDate: Date
var body: some View {
TabView(selection: $activeTab) {}
}
}
Now, we can create a ForEach
view with three tabs for each week. To do this, we will use our WeekPosition
enum. But first, we should add conformance to Hashable
and Identifiable
protocols.
public enum WeekPosition: Int, Identifiable, Hashable, CaseIterable {
case left = -7
case middle = 0
case right = 7
public var id: Int { rawValue }
}
Let’s return to our view, then. Create a ForEach
loop and put the WeekView
instance there. We can unwrap an optional Week
value with a dictionary’s subscript with the default
parameter. As a default value, we can create the default
static property.
public struct Week {
public let dates: [Date]
public let referenceDate: Date
public static let `default`: Self = Week.week(for: .now, at: .middle)
public init(dates: [Date], referenceDate: Date) {
self.dates = dates
self.referenceDate = referenceDate
}
public static func week(
for date: Date,
at position: WeekPosition
) -> Self {
let date = date.sameDay(at: position)
let startOfWeek = Calendar.current.date(from: Calendar.current.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date))
let weekDates = (0...6).compactMap {
Calendar.current.date(
byAdding: .day,
value: $0,
to: startOfWeek ?? .now
)
}
return Week(
dates: weekDates,
referenceDate: date
)
}
}
Remember to add a tag
to the WeekView
.
struct WeekCalendarView: View {
@State private var provider: WeekProvider = .init()
@State private var activeTab: WeekPosition = .middle
@State private var scrollDirection: WeekPosition = .middle
@Binding var selectedDate: Date
var body: some View {
TabView(selection: $activeTab) {
ForEach(WeekPosition.allCases) { position in
WeekView(
week: provider.weekDict[position, default: .default],
selectedDate: $selectedDate
)
.tag(position)
.frame(maxWidth: .infinity)
}
}
}
}
We will change the scrollDirection
value by switching the activeTab
, but only if the active tab is left or right. This will help us update the WeekProvider
with the correct data.
struct WeekCalendarView: View {
@State private var provider: WeekProvider = .init()
@State private var activeTab: WeekPosition = .middle
@State private var scrollDirection: WeekPosition = .middle
@Binding var selectedDate: Date
var body: some View {
TabView(selection: $activeTab) {
ForEach(WeekPosition.allCases) { position in
WeekView(
week: provider.weekDict[position, default: .default],
selectedDate: $selectedDate
)
.tag(position)
.frame(maxWidth: .infinity)
}
}
.onChange(of: activeTab) { _, newValue in
switch newValue {
case .left, .right:
scrollDirection = newValue
case .middle:
break
}
}
}
}
When we scroll, our active tab changes. Moving to the right or left from the middle tab, we should update our provider to recalculate weeks and re-render the view. After that, we must update our scrollDirection
value and switch the activeTab
to the middle. However, we should do this only after the middle tab disappears to ensure our UI is stable.
struct WeekCalendarView: View {
@State private var provider: WeekProvider = .init()
@State private var activeTab: WeekPosition = .middle
@State private var scrollDirection: WeekPosition = .middle
@Binding var selectedDate: Date
var body: some View {
TabView(selection: $activeTab) {
ForEach(WeekPosition.allCases) { position in
WeekView(
week: provider.weekDict[position, default: .default],
selectedDate: $selectedDate
)
.tag(position)
.frame(maxWidth: .infinity)
.onDisappear {
guard position == .middle, scrollDirection != .middle else { return }
provider.update(by: scrollDirection)
scrollDirection = .middle
activeTab = .middle
}
}
}
.onChange(of: activeTab) { _, newValue in
switch newValue {
case .left, .right:
scrollDirection = newValue
case .middle:
break
}
}
}
}
Handle date selection
As we discussed, when users choose a date, we must update its value in the provider. Since we have a binding property, we can track its changes and update the provider’s date.
struct WeekCalendarView: View {
@State private var provider: WeekProvider = .init()
@State private var activeTab: WeekPosition = .middle
@State private var scrollDirection: WeekPosition = .middle
@Binding var selectedDate: Date
var body: some View {
TabView(selection: $activeTab) {
ForEach(WeekPosition.allCases) { position in
WeekView(
week: provider.weekDict[position, default: .default],
selectedDate: $selectedDate
)
.tag(position)
.frame(maxWidth: .infinity)
.onDisappear {
guard position == .middle, scrollDirection != .middle else { return }
provider.update(by: scrollDirection)
scrollDirection = .middle
activeTab = .middle
}
}
}
.onChange(of: activeTab) { _, newValue in
switch newValue {
case .left, .right:
scrollDirection = newValue
case .middle:
break
}
}
.onChange(of: selectedDate) { _, newValue in
provider.setDate(newValue)
}
}
}
The final touch
Now, we can hide TabView
indicators. And we are good to go.
struct WeekCalendarView: View {
@State private var provider: WeekProvider = .init()
@State private var activeTab: WeekPosition = .middle
@State private var scrollDirection: WeekPosition = .middle
@Binding var selectedDate: Date
var body: some View {
TabView(selection: $activeTab) {
ForEach(WeekPosition.allCases) { position in
WeekView(
week: provider.weekDict[position, default: .default],
selectedDate: $selectedDate
)
.tag(position)
.frame(maxWidth: .infinity)
.onDisappear {
guard position == .middle, scrollDirection != .middle else { return }
provider.update(by: scrollDirection)
scrollDirection = .middle
activeTab = .middle
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.onChange(of: activeTab) { _, newValue in
switch newValue {
case .left, .right:
scrollDirection = newValue
case .middle:
break
}
}
.onChange(of: selectedDate) { _, newValue in
provider.setDate(newValue)
}
}
}
Conclusion
We are almost finished with our views. It’s time to add some customization tools.
If you enjoyed this article, please feel free to follow me on my social media: