Skip to content

Commit 1fbb049

Browse files
Mark Pospeselmpospese
Mark Pospesel
authored andcommitted
[CM-1189] add intrinsic content size, rename header view
1 parent 7863fe7 commit 1fbb049

10 files changed

+167
-60
lines changed

Sources/YCalendarPicker/Classes/YCalendarPicker+Appearance.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ extension YCalendarPicker {
4848
/// - selectedDayAppearance: Appearance for selected day. Default is `.Defaults.selected`.
4949
/// - disabledDayAppearance: Appearance for disabled day. Default is `.Defaults.disabled`.
5050
/// - bookedDayAppearance: Appearance for already booked day. Default is `Defaults.booked`.
51-
/// - weekdayStyle: Typography and text color for weekdays name. Default is `DefaultStyles.weekday`.
51+
/// - weekdayStyle: Typography and text color for weekday names. Default is `DefaultStyles.weekday`.
5252
/// - previousImage: Previous button image. Default is `Appearance.defaultPreviousImage`.
5353
/// - nextImage: Next button image. Default is `Appearance.defaultNextImage`.
54-
/// - monthStyle: Typography and text color for Month name in header. Default is `DefaultStyles.month`.
54+
/// - monthStyle: Typography and text color for Month name. Default is `DefaultStyles.month`.
5555
/// - backgroundColor: Background color for calendar view. Default is `.systemBackground`.
5656
public init(
5757
normalDayAppearance: Day = .Defaults.normal,

Sources/YCalendarPicker/Classes/YCalendarPicker.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,37 @@ public class YCalendarPicker: UIControl {
8282
super.init(coder: coder)
8383
addCalendarView()
8484
}
85+
86+
/// Calculates the intrinsic size of the calendar picker
87+
override public var intrinsicContentSize: CGSize {
88+
let monthLayout = appearance.monthStyle.typography.generateLayout(
89+
maximumScaleFactor: MonthView.maximumScaleFactor,
90+
compatibleWith: traitCollection
91+
)
92+
let weekdayLayout = appearance.weekdayStyle.typography.generateLayout(
93+
maximumScaleFactor: WeekdayView.maximumScaleFactor,
94+
compatibleWith: traitCollection
95+
)
96+
97+
let daySize = DayView.size.outset(by: NSDirectionalEdgeInsets(all: DayView.padding))
98+
99+
let width = 7 * daySize.width
100+
101+
let monthHeight = max(monthLayout.lineHeight, MonthView.minimumButtonSize.height)
102+
let weekdayHeight = weekdayLayout.lineHeight + (2 * WeekdayView.verticalPadding)
103+
let daysHeight = 6 * daySize.height
104+
let height = monthHeight + weekdayHeight + daysHeight
105+
106+
return CGSize(width: width, height: height)
107+
}
108+
109+
/// Adjusts the intrinsic content size on Dynamic Type changes
110+
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
111+
super.traitCollectionDidChange(previousTraitCollection)
112+
if traitCollection.hasDifferentFontAppearance(comparedTo: previousTraitCollection) {
113+
invalidateIntrinsicContentSize()
114+
}
115+
}
85116
}
86117

87118
private extension YCalendarPicker {

Sources/YCalendarPicker/Views/DayView.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ import SwiftUI
1010
import YMatterType
1111

1212
struct DayView {
13+
/// maximum scale factor of the month-year text label
14+
static let maximumScaleFactor: CGFloat = 1.5
15+
/// size of each day
16+
static let size = CGSize(width: 40, height: 40)
17+
/// horizontal and vertical padding around day view circles
18+
static let padding: CGFloat = 2
19+
1320
let appearance: YCalendarPicker.Appearance
1421
let dateItem: CalendarMonthItem
1522
let locale: Locale
@@ -27,18 +34,18 @@ extension DayView: View {
2734
TextStyleLabel(dateItem.day, typography: appearance.typography, configuration: { label in
2835
label.isUserInteractionEnabled = true
2936
label.textAlignment = .center
30-
label.maximumScaleFactor = 1.5
37+
label.maximumScaleFactor = DayView.maximumScaleFactor
3138
label.textColor = appearance.foregroundColor
3239
})
33-
Spacer().frame(minWidth: 40, minHeight: 40)
40+
Spacer().frame(minWidth: DayView.size.width, minHeight: DayView.size.height)
3441
}
3542
.background(
3643
Circle()
3744
.stroke(Color(appearance.borderColor), lineWidth: appearance.borderWidth)
3845
.background(Circle().foregroundColor(Color(appearance.backgroundColor)))
3946
)
40-
.padding(.horizontal, 2.0)
41-
.padding(.vertical, 2.0)
47+
.padding(.horizontal, DayView.padding)
48+
.padding(.vertical, DayView.padding)
4249
.onTapGesture {
4350
guard dateItem.isEnabled else { return }
4451
selectedDate = dateItem.date

Sources/YCalendarPicker/Views/CalendarHeaderView.swift renamed to Sources/YCalendarPicker/Views/MonthView.swift

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// CalendarHeaderView.swift
2+
// MonthView.swift
33
// YCalendarPicker
44
//
55
// Created by Sahil Saini on 29/11/22.
@@ -9,11 +9,16 @@
99
import SwiftUI
1010
import YMatterType
1111

12-
/// Calendar header for month and year
13-
internal struct CalendarHeaderView {
12+
/// Displays current month and year together with previous and next buttons
13+
internal struct MonthView {
14+
/// maximum scale factor of the month-year text label
15+
static let maximumScaleFactor: CGFloat = 1.5
16+
/// minimum size for previous and next buttons
17+
static let minimumButtonSize = CGSize(width: 44, height: 44)
18+
1419
@Binding var currentDate: Date
1520
var appearance: YCalendarPicker.Appearance
16-
let headerDateFormat: String
21+
let dateFormat: String
1722
let minimumDate: Date?
1823
let maximumDate: Date?
1924
let locale: Locale
@@ -38,17 +43,17 @@ internal struct CalendarHeaderView {
3843
}
3944
}
4045

41-
extension CalendarHeaderView: View {
46+
extension MonthView: View {
4247
var body: some View {
43-
getHeader()
48+
getMonthView()
4449
}
4550

4651
@ViewBuilder
47-
func getHeader() -> some View {
52+
func getMonthView() -> some View {
4853
HStack {
4954
TextStyleLabel(getMonthAndYear(), typography: appearance.monthStyle.typography, configuration: { label in
5055
label.textColor = appearance.monthStyle.textColor
51-
label.maximumScaleFactor = 1.5
56+
label.maximumScaleFactor = MonthView.maximumScaleFactor
5257
})
5358

5459
Spacer()
@@ -61,7 +66,10 @@ extension CalendarHeaderView: View {
6166
.foregroundColor(Color(appearance.monthStyle.textColor))
6267
.opacity(isPreviousButtonDisabled ? 0.5 : 1.0)
6368
})
64-
.frame(minWidth: 44, minHeight: 44)
69+
.frame(
70+
minWidth: MonthView.minimumButtonSize.width,
71+
minHeight: MonthView.minimumButtonSize.height
72+
)
6573
.disabled(isPreviousButtonDisabled)
6674
.accessibilityLabel(YCalendarPicker.Strings.previousMonthA11yLabel.localized)
6775
Button(action: {
@@ -71,7 +79,10 @@ extension CalendarHeaderView: View {
7179
.foregroundColor(Color(appearance.monthStyle.textColor))
7280
.opacity(isNextButtonDisabled ? 0.5 : 1.0)
7381
})
74-
.frame(minWidth: 44, minHeight: 44)
82+
.frame(
83+
minWidth: MonthView.minimumButtonSize.width,
84+
minHeight: MonthView.minimumButtonSize.height
85+
)
7586
.disabled(isNextButtonDisabled)
7687
.accessibilityLabel(YCalendarPicker.Strings.nextMonthA11yLabel.localized)
7788
}
@@ -98,16 +109,16 @@ extension CalendarHeaderView: View {
98109
}
99110

100111
func getMonthAndYear() -> String {
101-
currentDate.toString(withTemplate: headerDateFormat, locale: locale) ?? ""
112+
currentDate.toString(withTemplate: dateFormat, locale: locale) ?? ""
102113
}
103114
}
104115

105-
struct CalendarHeaderView_Previews: PreviewProvider {
116+
struct MonthView_Previews: PreviewProvider {
106117
static var previews: some View {
107-
CalendarHeaderView(
118+
MonthView(
108119
currentDate: .constant(Date()),
109120
appearance: .default,
110-
headerDateFormat: "MMMMyyyy",
121+
dateFormat: "MMMMyyyy",
111122
minimumDate: nil,
112123
maximumDate: nil,
113124
locale: Locale(identifier: "pt_BR")

Sources/YCalendarPicker/Views/WeekdayView.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import YMatterType
1111

1212
/// WeekdayView is the view shown for weekday names at top of days/dates
1313
internal struct WeekdayView {
14-
var firstWeekday = 0
14+
/// maximum scale factor of the week day text labels
15+
static let maximumScaleFactor: CGFloat = 1.33
16+
/// vertical padding around week day text labels
17+
static let verticalPadding: CGFloat = 2
18+
19+
var firstWeekday: Int
1520
var appearance: YCalendarPicker.Appearance
1621
let weekdayNames: [String]
1722

@@ -42,14 +47,14 @@ extension WeekdayView: View {
4247
getWeekText(for: index.modulo(7))
4348
}
4449
}
45-
.padding(.vertical, 2)
50+
.padding(.vertical, WeekdayView.verticalPadding)
4651
}
4752

4853
@ViewBuilder
4954
func getWeekText(for index: Int) -> some View {
5055
TextStyleLabel(weekdayNames[index], typography: appearance.weekdayStyle.typography, configuration: { label in
5156
label.textAlignment = .center
52-
label.maximumScaleFactor = 1.33
57+
label.maximumScaleFactor = WeekdayView.maximumScaleFactor
5358
label.textColor = appearance.weekdayStyle.textColor
5459
})
5560
}

Sources/YCalendarPicker/Views/YCalendarView.swift

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ extension YCalendarView: View {
105105
/// :nodoc:
106106
public var body: some View {
107107
VStack(spacing: 0) {
108-
getHeader()
108+
getMonthView()
109109
getWeekdayView()
110-
getDateView()
110+
getDaysView()
111111
}.gesture(
112112
DragGesture().onEnded({ value in
113113
currentDate = getCurrentDateAfterSwipe(swipeValue: value.translation)
@@ -116,21 +116,24 @@ extension YCalendarView: View {
116116
.background(Color(self.appearance.backgroundColor))
117117
}
118118

119-
/// This function creates a header view
120-
/// - Returns: A view with month name, next and previous month button
121119
@ViewBuilder
122-
func getHeader() -> some View {
123-
CalendarHeaderView(
120+
func getMonthView() -> some View {
121+
MonthView(
124122
currentDate: $currentDate,
125123
appearance: appearance,
126-
headerDateFormat: headerDateFormat,
124+
dateFormat: headerDateFormat,
127125
minimumDate: minimumDate,
128126
maximumDate: maximumDate,
129127
locale: locale
130128
)
131129
}
132-
133-
func getDateView() -> some View {
130+
131+
@ViewBuilder
132+
func getWeekdayView() -> some View {
133+
WeekdayView(firstWeekday: firstWeekday, appearance: appearance, locale: locale)
134+
}
135+
136+
func getDaysView() -> some View {
134137
var allDates = currentDate.getAllDatesForSelectedMonth(firstWeekIndex: firstWeekday.modulo(7))
135138
allDates = allDates.map { dateItem -> CalendarMonthItem in
136139
var newItem = dateItem
@@ -161,11 +164,6 @@ extension YCalendarView: View {
161164
monthDidChange(newValue.dateOnly)
162165
}
163166
}
164-
165-
@ViewBuilder
166-
func getWeekdayView() -> some View {
167-
WeekdayView(firstWeekday: firstWeekday, appearance: appearance, locale: locale)
168-
}
169167
}
170168

171169
extension YCalendarView {

Tests/YCalendarPickerTests/Classes/YCalendarPickerTests.swift

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,37 @@ final class YCalendarPickerTests: XCTestCase {
105105
sut2.calendarView.monthDidChange(date1)
106106
XCTAssertNotEqual(sut.calendarView.currentDate, date2)
107107
}
108+
109+
func testIntrinsicContentSize() {
110+
let sut = makeSUT()
111+
112+
let size = sut.intrinsicContentSize
113+
let daySize = DayView.size.outset(by: NSDirectionalEdgeInsets(all: DayView.padding))
114+
let monthHeight = MonthView.minimumButtonSize.height
115+
let weekdayHeight = sut.appearance.weekdayStyle.typography.lineHeight + 2 * WeekdayView.verticalPadding
116+
let daysHeight = 6 * daySize.height
117+
118+
XCTAssertEqual(size.width, 7 * daySize.width)
119+
XCTAssertEqual(size.height, monthHeight + weekdayHeight + daysHeight)
120+
}
121+
122+
func testRespondsToDynamicTypeChanges() {
123+
let sut = makeSUT()
124+
125+
let oldSize = sut.intrinsicContentSize
126+
127+
// create some nested view controllers so that we can override traits
128+
let (parent, child) = makeNestedViewControllers(subview: sut)
129+
130+
let traits = UITraitCollection(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge) // really large
131+
parent.setOverrideTraitCollection(traits, forChild: child)
132+
sut.traitCollectionDidChange(traits)
133+
134+
let newSize = sut.intrinsicContentSize
135+
136+
XCTAssertEqual(newSize.width, oldSize.width)
137+
XCTAssertGreaterThan(newSize.height, oldSize.height)
138+
}
108139
}
109140

110141
private extension YCalendarPickerTests {
@@ -116,7 +147,7 @@ private extension YCalendarPickerTests {
116147
line: UInt = #line
117148
) -> YCalendarPicker {
118149
let sut = YCalendarPicker(minimumDate: minDate, maximumDate: maxDate)
119-
trackForMemoryLeaks(sut, file: file, line: line)
150+
trackForMemoryLeak(sut, file: file, line: line)
120151
return sut
121152
}
122153

@@ -130,7 +161,32 @@ private extension YCalendarPickerTests {
130161
return nil
131162
}
132163
guard let coder = try? NSKeyedUnarchiver(forReadingFrom: data) else { return nil }
133-
trackForMemoryLeaks(sut, file: file, line: line)
164+
trackForMemoryLeak(sut, file: file, line: line)
134165
return YCalendarPicker(coder: coder)
135166
}
167+
168+
/// Create nested view controllers containing the view to be tested so that we can override traits
169+
func makeNestedViewControllers(
170+
subview: UIView,
171+
file: StaticString = #filePath,
172+
line: UInt = #line
173+
) -> (parent: UIViewController, child: UIViewController) {
174+
let parent = UIViewController()
175+
let child = UIViewController()
176+
parent.addChild(child)
177+
parent.view.addSubview(child.view)
178+
179+
// constrain child controller view to parent
180+
child.view.constrainEdges()
181+
182+
child.view.addSubview(subview)
183+
184+
// constrain subview to child view center
185+
subview.constrainCenter()
186+
187+
trackForMemoryLeak(parent, file: file, line: line)
188+
trackForMemoryLeak(child, file: file, line: line)
189+
190+
return (parent, child)
191+
}
136192
}

Tests/YCalendarPickerTests/Test Helpers/XCTestCase+MemoryLeakTracking.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import XCTest
1010

1111
extension XCTestCase {
12-
func trackForMemoryLeaks(_ instance: AnyObject, file: StaticString = #filePath, line: UInt = #line) {
12+
func trackForMemoryLeak(_ instance: AnyObject, file: StaticString = #filePath, line: UInt = #line) {
1313
addTeardownBlock { [weak instance] in
1414
XCTAssertNil(
1515
instance,

0 commit comments

Comments
 (0)