From cfd5dff9e3a17ce1f4ea15ed556290e4f77350cb Mon Sep 17 00:00:00 2001 From: Roberto Zunica Date: Thu, 11 Dec 2025 23:59:26 +0100 Subject: [PATCH 1/3] added text tags icons not working yet --- QVRWeekView/Assets/tags/bed.svg | 3 + QVRWeekView/Assets/tags/fail.svg | 3 + QVRWeekView/Assets/tags/success.svg | 3 + QVRWeekView/Classes/Common/EventData.swift | 27 +++- QVRWeekView/Classes/Common/EventLayer.swift | 151 +++++++++++++++++++- 5 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 QVRWeekView/Assets/tags/bed.svg create mode 100644 QVRWeekView/Assets/tags/fail.svg create mode 100644 QVRWeekView/Assets/tags/success.svg diff --git a/QVRWeekView/Assets/tags/bed.svg b/QVRWeekView/Assets/tags/bed.svg new file mode 100644 index 0000000..c3ee004 --- /dev/null +++ b/QVRWeekView/Assets/tags/bed.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/QVRWeekView/Assets/tags/fail.svg b/QVRWeekView/Assets/tags/fail.svg new file mode 100644 index 0000000..9b95147 --- /dev/null +++ b/QVRWeekView/Assets/tags/fail.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/QVRWeekView/Assets/tags/success.svg b/QVRWeekView/Assets/tags/success.svg new file mode 100644 index 0000000..3c44a2b --- /dev/null +++ b/QVRWeekView/Assets/tags/success.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/QVRWeekView/Classes/Common/EventData.swift b/QVRWeekView/Classes/Common/EventData.swift index c691310..a6ece7e 100644 --- a/QVRWeekView/Classes/Common/EventData.swift +++ b/QVRWeekView/Classes/Common/EventData.swift @@ -26,6 +26,8 @@ open class EventData: NSObject, NSCoding { public let color: UIColor // Stores if event is an all day event public let allDay: Bool + // Tags associated with the event + public let tags: [String] // Stores an optional gradient layer which will be used to draw event. Can only be set once. private(set) var gradientLayer: CAGradientLayer? { didSet { gradientLayer = oldValue ?? gradientLayer } } @@ -37,12 +39,13 @@ open class EventData: NSObject, NSCoding { /** Main initializer. All properties. */ - public init(id: String, title: String, startDate: Date, endDate: Date, location: String, color: UIColor, allDay: Bool, gradientLayer: CAGradientLayer? = nil) { + public init(id: String, title: String, startDate: Date, endDate: Date, location: String, color: UIColor, allDay: Bool, tags: [String] = [], gradientLayer: CAGradientLayer? = nil) { self.id = id self.title = title self.location = location self.color = color self.allDay = allDay + self.tags = tags guard startDate.compare(endDate).rawValue <= 0 else { self.startDate = startDate self.endDate = startDate @@ -119,6 +122,7 @@ open class EventData: NSObject, NSCoding { coder.encode(location, forKey: EventDataEncoderKey.location) coder.encode(color, forKey: EventDataEncoderKey.color) coder.encode(allDay, forKey: EventDataEncoderKey.allDay) + coder.encode(tags, forKey: EventDataEncoderKey.tags) coder.encode(gradientLayer, forKey: EventDataEncoderKey.gradientLayer) } @@ -131,6 +135,7 @@ open class EventData: NSObject, NSCoding { let dColor = coder.decodeObject(forKey: EventDataEncoderKey.color) as? UIColor { let dGradientLayer = coder.decodeObject(forKey: EventDataEncoderKey.gradientLayer) as? CAGradientLayer let dAllDay = coder.decodeBool(forKey: EventDataEncoderKey.allDay) + let dTags = coder.decodeObject(forKey: EventDataEncoderKey.tags) as? [String] ?? [] self.init(id: dId, title: dTitle, startDate: dStartDate, @@ -138,6 +143,7 @@ open class EventData: NSObject, NSCoding { location: dLocation, color: dColor, allDay: dAllDay, + tags: dTags, gradientLayer: dGradientLayer) } else { return nil @@ -152,7 +158,8 @@ open class EventData: NSObject, NSCoding { (lhs.title == rhs.title) && (lhs.location == rhs.location) && (lhs.allDay == rhs.allDay) && - (lhs.color.isEqual(rhs.color)) + (lhs.color.isEqual(rhs.color)) && + (lhs.tags == rhs.tags) } public override var hash: Int { @@ -167,8 +174,13 @@ open class EventData: NSObject, NSCoding { open func getDisplayString(withMainFont mainFont: UIFont, infoFont: UIFont, andColor color: UIColor) -> NSAttributedString { let df = DateFormatter() df.dateFormat = "HH:mm" - let mainFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: mainFont, NSAttributedString.Key.foregroundColor: color.cgColor] - let infoFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: infoFont, NSAttributedString.Key.foregroundColor: color.cgColor] + + // Use Montserrat Bold for title, Montserrat Medium for description + let titleFont = UIFont(name: "Montserrat-Bold", size: 12) ?? UIFont.boldSystemFont(ofSize: 12) + let descFont = UIFont(name: "Montserrat-Medium", size: 10) ?? UIFont.systemFont(ofSize: 10, weight: .medium) + + let mainFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: titleFont, NSAttributedString.Key.foregroundColor: UIColor.white] + let infoFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: descFont, NSAttributedString.Key.foregroundColor: UIColor.white] let mainAttributedString = NSMutableAttributedString(string: self.title, attributes: mainFontAttributes) if !self.allDay { mainAttributedString.append(NSMutableAttributedString( @@ -204,19 +216,19 @@ open class EventData: NSObject, NSCoding { } public func remakeEventData(withStart start: Date, andEnd end: Date) -> EventData { - let newEvent = EventData(id: self.id, title: self.title, startDate: start, endDate: end, location: self.location, color: self.color, allDay: self.allDay) + let newEvent = EventData(id: self.id, title: self.title, startDate: start, endDate: end, location: self.location, color: self.color, allDay: self.allDay, tags: self.tags) newEvent.configureGradient(self.gradientLayer) return newEvent } public func remakeEventData(withColor color: UIColor) -> EventData { - let newEvent = EventData(id: self.id, title: self.title, startDate: self.startDate, endDate: self.endDate, location: self.location, color: color, allDay: self.allDay) + let newEvent = EventData(id: self.id, title: self.title, startDate: self.startDate, endDate: self.endDate, location: self.location, color: color, allDay: self.allDay, tags: self.tags) newEvent.configureGradient(self.gradientLayer) return newEvent } public func remakeEventDataAsAllDay(forDate date: Date) -> EventData { - let newEvent = EventData(id: self.id, title: self.title, startDate: date.getStartOfDay(), endDate: date.getEndOfDay(), location: self.location, color: self.color, allDay: true) + let newEvent = EventData(id: self.id, title: self.title, startDate: date.getStartOfDay(), endDate: date.getEndOfDay(), location: self.location, color: self.color, allDay: true, tags: self.tags) newEvent.configureGradient(self.gradientLayer) return newEvent } @@ -297,5 +309,6 @@ struct EventDataEncoderKey { static let location = "EVENT_DATA_LOCATION" static let color = "EVENT_DATA_COLOR" static let allDay = "EVENT_DATA_ALL_DAY" + static let tags = "EVENT_DATA_TAGS" static let gradientLayer = "EVENT_DATA_GRADIENT_LAYER" } diff --git a/QVRWeekView/Classes/Common/EventLayer.swift b/QVRWeekView/Classes/Common/EventLayer.swift index 099f6ef..ba49f1c 100644 --- a/QVRWeekView/Classes/Common/EventLayer.swift +++ b/QVRWeekView/Classes/Common/EventLayer.swift @@ -25,6 +25,9 @@ class EventLayer: CALayer { self.backgroundColor = event.color.cgColor } + let xPadding = layout.eventLabelHorizontalTextPadding + let yPadding = layout.eventLabelVerticalTextPadding + // Configure event text layer let eventTextLayer = CATextLayer() eventTextLayer.isWrapped = true @@ -33,15 +36,157 @@ class EventLayer: CALayer { withMainFont: layout.eventLabelFont, infoFont: layout.eventLabelInfoFont, andColor: layout.eventLabelTextColor) - - let xPadding = layout.eventLabelHorizontalTextPadding - let yPadding = layout.eventLabelVerticalTextPadding + eventTextLayer.frame = CGRect( x: frame.origin.x + xPadding, y: frame.origin.y + yPadding, width: frame.width - 2 * xPadding, height: frame.height - 2 * yPadding) self.addSublayer(eventTextLayer) + + // Add tags at the bottom if available + if !event.tags.isEmpty { + let tagHeight: CGFloat = 18 + let bottomMargin: CGFloat = 4 + let tagsY = frame.origin.y + frame.height - yPadding - tagHeight - bottomMargin + + // Only render tags if there's enough space + if tagsY > frame.origin.y + yPadding + 20 { + addTagsLayers( + tags: event.tags, + x: frame.origin.x + xPadding, + y: tagsY, + maxWidth: frame.width - 2 * xPadding, + tagHeight: tagHeight, + eventColor: event.color) + } + } + } + + private func addTagsLayers(tags: [String], x: CGFloat, y: CGFloat, maxWidth: CGFloat, tagHeight: CGFloat, eventColor: UIColor) { + let tagSpacing: CGFloat = 4 + let tagPadding: CGFloat = 6 + let tagCornerRadius: CGFloat = tagHeight / 2 + let iconSize: CGFloat = tagHeight // Icons same height as pills + + var currentX: CGFloat = x + + for tag in tags { + let tagLower = tag.lowercased() + let iconName = getIconForTag(tagLower) + var iconImage: UIImage? = nil + + // Try to load icon if it exists + if let iconName = iconName { + iconImage = loadIconImage(named: iconName) + } + + // Calculate tag width + var tagWidth: CGFloat + let tagFont = UIFont(name: "Montserrat-Medium", size: 10) ?? UIFont.systemFont(ofSize: 10, weight: .medium) + + if iconImage != nil { + tagWidth = iconSize // Just icon width, no padding + } else { + // Text only tags with padding (fallback when icon not found) + let tagText = tag as NSString + let textWidth = tagText.size(withAttributes: [.font: tagFont]).width + tagWidth = textWidth + (tagPadding * 2) + } + + // Check if tag fits on current line + if currentX + tagWidth > x + maxWidth { + break // Stop if doesn't fit + } + + if let image = iconImage { + // Create icon layer from Assets (no background pill) + let iconLayer = CALayer() + iconLayer.contents = image.cgImage + iconLayer.frame = CGRect( + x: currentX, + y: y, + width: iconSize, + height: iconSize + ) + iconLayer.contentsGravity = .resizeAspect + // Use destination out blend mode for transparent icons + iconLayer.compositingFilter = "destinationOut" + self.addSublayer(iconLayer) + } else { + // Create tag background layer (white pill for text fallback) + let tagBackgroundLayer = CALayer() + tagBackgroundLayer.frame = CGRect(x: currentX, y: y, width: tagWidth, height: tagHeight) + tagBackgroundLayer.backgroundColor = UIColor.white.cgColor + tagBackgroundLayer.cornerRadius = tagCornerRadius + self.addSublayer(tagBackgroundLayer) + + // Create tag text layer with Montserrat Medium + let tagTextLayer = CATextLayer() + let tagText = tag as NSString + let textWidth = tagText.size(withAttributes: [.font: tagFont]).width + + tagTextLayer.frame = CGRect( + x: currentX + tagPadding, + y: y + 3, + width: textWidth, + height: tagHeight - 6 + ) + tagTextLayer.string = tag + tagTextLayer.font = tagFont + tagTextLayer.fontSize = 10 + tagTextLayer.foregroundColor = eventColor.cgColor + tagTextLayer.contentsScale = UIScreen.main.scale + tagTextLayer.alignmentMode = .center + self.addSublayer(tagTextLayer) + } + + // Move x position for next tag + currentX += tagWidth + tagSpacing + } + } + + private func getIconForTag(_ tag: String) -> String? { + switch tag { + case "bed": + return "bed" + case "alert": + return "alert" + case "fail": + return "fail" + case "success": + return "success" + case "drink": + return "drink" + default: + return nil + } + } + + private func loadIconImage(named: String) -> UIImage? { + let bundle = Bundle(for: EventLayer.self) + + // Try SVG first + if let svgPath = bundle.path(forResource: named, ofType: "svg", inDirectory: "Assets/tags"), + let svgData = try? Data(contentsOf: URL(fileURLWithPath: svgPath)) { + if #available(iOS 13.0, *), let image = UIImage(data: svgData) { + return image + } + } + + // Try PNG + if let pngPath = bundle.path(forResource: named, ofType: "png", inDirectory: "Assets/tags"), + let image = UIImage(contentsOfFile: pngPath) { + return image + } + + // Try PDF + if let pdfPath = bundle.path(forResource: named, ofType: "pdf", inDirectory: "Assets/tags"), + let image = UIImage(contentsOfFile: pdfPath) { + return image + } + + return nil } required init?(coder aDecoder: NSCoder) { From 2d5243c615d91c9b9a13ae777a5f934c7ffcbd5b Mon Sep 17 00:00:00 2001 From: Roberto Zunica Date: Sun, 14 Dec 2025 17:14:51 +0100 Subject: [PATCH 2/3] added automatic image search for tag icons --- QVRWeekView/Assets/.gitkeep | 0 QVRWeekView/Assets/tags/bed.svg | 3 -- QVRWeekView/Assets/tags/fail.svg | 3 -- QVRWeekView/Assets/tags/success.svg | 3 -- QVRWeekView/Classes/Common/EventLayer.swift | 51 ++++++--------------- README.md | 45 ++++++++++++++++++ 6 files changed, 60 insertions(+), 45 deletions(-) delete mode 100644 QVRWeekView/Assets/.gitkeep delete mode 100644 QVRWeekView/Assets/tags/bed.svg delete mode 100644 QVRWeekView/Assets/tags/fail.svg delete mode 100644 QVRWeekView/Assets/tags/success.svg diff --git a/QVRWeekView/Assets/.gitkeep b/QVRWeekView/Assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/QVRWeekView/Assets/tags/bed.svg b/QVRWeekView/Assets/tags/bed.svg deleted file mode 100644 index c3ee004..0000000 --- a/QVRWeekView/Assets/tags/bed.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/QVRWeekView/Assets/tags/fail.svg b/QVRWeekView/Assets/tags/fail.svg deleted file mode 100644 index 9b95147..0000000 --- a/QVRWeekView/Assets/tags/fail.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/QVRWeekView/Assets/tags/success.svg b/QVRWeekView/Assets/tags/success.svg deleted file mode 100644 index 3c44a2b..0000000 --- a/QVRWeekView/Assets/tags/success.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/QVRWeekView/Classes/Common/EventLayer.swift b/QVRWeekView/Classes/Common/EventLayer.swift index ba49f1c..b751e4d 100644 --- a/QVRWeekView/Classes/Common/EventLayer.swift +++ b/QVRWeekView/Classes/Common/EventLayer.swift @@ -73,13 +73,9 @@ class EventLayer: CALayer { for tag in tags { let tagLower = tag.lowercased() - let iconName = getIconForTag(tagLower) - var iconImage: UIImage? = nil - // Try to load icon if it exists - if let iconName = iconName { - iconImage = loadIconImage(named: iconName) - } + // Try to load icon for any tag (automatically detects from Images.xcassets/tags/) + let iconImage = loadIconImage(named: tagLower) // Calculate tag width var tagWidth: CGFloat @@ -146,43 +142,26 @@ class EventLayer: CALayer { } } - private func getIconForTag(_ tag: String) -> String? { - switch tag { - case "bed": - return "bed" - case "alert": - return "alert" - case "fail": - return "fail" - case "success": - return "success" - case "drink": - return "drink" - default: - return nil - } - } - private func loadIconImage(named: String) -> UIImage? { - let bundle = Bundle(for: EventLayer.self) + // Try to load from main app bundle under tags namespace (Images.xcassets/tags/) + if let image = UIImage(named: "tags/\(named)", in: Bundle.main, compatibleWith: nil) { + return image + } - // Try SVG first - if let svgPath = bundle.path(forResource: named, ofType: "svg", inDirectory: "Assets/tags"), - let svgData = try? Data(contentsOf: URL(fileURLWithPath: svgPath)) { - if #available(iOS 13.0, *), let image = UIImage(data: svgData) { - return image - } + // Try without namespace in main bundle + if let image = UIImage(named: named, in: Bundle.main, compatibleWith: nil) { + return image } - // Try PNG - if let pngPath = bundle.path(forResource: named, ofType: "png", inDirectory: "Assets/tags"), - let image = UIImage(contentsOfFile: pngPath) { + // Try from framework bundle under tags namespace + let bundle = Bundle(for: EventLayer.self) + + if let image = UIImage(named: "tags/\(named)", in: bundle, compatibleWith: nil) { return image } - // Try PDF - if let pdfPath = bundle.path(forResource: named, ofType: "pdf", inDirectory: "Assets/tags"), - let image = UIImage(contentsOfFile: pdfPath) { + // Try without namespace in framework bundle + if let image = UIImage(named: named, in: bundle, compatibleWith: nil) { return image } diff --git a/README.md b/README.md index 8008507..8da28b8 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,51 @@ Below is a table of all customizable properties of the `WeekView` | velocityOffsetMultiplier:`CGFloat` | Sensitivity for horizontal scrolling. A higher number will multiply input velocity more and thus result in more cells being skipped when scrolling. | `0.75` | | horizontalScrolling:`HorizontalScrolling` | Used to determine horizontal scrolling behaviour. `.infinite` is infinite scrolling, `.finite(number, startDate)` is finite scrolling for a given number of days from the starting date. | `.infinite` +### Event Tags + +Events support tags which are displayed at the bottom of event cells. Tags can be text labels or icons. + +#### Using Tags + +Add tags to events by passing a string array: + +```swift +let event = EventData( + id: "1", + title: "Meeting", + startDate: startDate, + endDate: endDate, + location: "Room 101", + color: .blue, + allDay: false, + tags: ["Work", "Important"] +) +``` + +#### Custom Tag Icons + +The library includes built-in icon support for: `bed`, `alert`, `fail`, `success`, `drink`. These tags will display as icons instead of text. If you add them to your app's Assets.xcassets + +To add your own custom tag icons: + +1. **Add to your app's Asset Catalog** (Recommended): + - Open your app's `Assets.xcassets` + - Add a new Image Set for each icon (e.g., "meeting", "personal") + - Add PNG or PDF images to the image sets + - Use the image set name as the tag name + +2. **Using PNG/PDF files**: + - Add PNG or PDF files to your app bundle + - Name them to match your tag names (e.g., "meeting.png") + - The library will automatically find and use them + +The library searches for icons in this order: +1. Main app bundle's Asset Catalog +2. Framework bundle's Asset Catalog +3. Framework's Assets/tags directory (PNG/PDF) + +Tags without matching icons will be displayed as text pills with the event color. + ## How it works The main WeekView view is a subclass of UIView. The view layout is retrieved from the WeekView xib file. WeekView contains a top and side bar sub view. The side bar contains an HourSideBarView which displays the hours. WeekView also contains a DayScrollView (UIScrollView subclass) which controls vertical scrolling and also delegates and contains a DayCollectionView (UICollectionView subclass) which controls the horizontal scrolling. DayCollectionView cells are DayViewCells, whose view is generated programtically (due to inefficiencies caused by auto-layout). From 51c05fe4d774ca8a416bc2f3b2c204a750cc5316 Mon Sep 17 00:00:00 2001 From: Roberto Zunica Date: Sun, 14 Dec 2025 17:15:30 +0100 Subject: [PATCH 3/3] update version number --- QVRWeekView.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QVRWeekView.podspec b/QVRWeekView.podspec index ef17c04..72d4778 100644 --- a/QVRWeekView.podspec +++ b/QVRWeekView.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'QVRWeekView' -s.version = '0.14.2' +s.version = '0.15.0' s.summary = 'QVRWeekView is a simple calendar week view with support for horizontal, vertical scrolling and zooming.' s.swift_version = '5'