From ab30c7d7c56b1453f62b0b4b1ed53cc6567f225f Mon Sep 17 00:00:00 2001 From: RareScrap Date: Tue, 20 Oct 2020 10:37:36 +0800 Subject: [PATCH 1/4] feat(view): First sketch of support for custom event view --- .../CalendarApp.xcodeproj/project.pbxproj | 22 +++++++++- .../CustomCalendarExampleController.swift | 10 ++++- .../CustomEventView/MyEventView.swift | 30 +++++++++++++ .../CustomEventView/MyEventView.xib | 44 +++++++++++++++++++ Source/Timeline/EventView.swift | 2 +- Source/Timeline/TimelinePagerView.swift | 2 + Source/Timeline/TimelineView.swift | 24 ++++++++-- 7 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift create mode 100644 CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib diff --git a/CalendarKitDemo/CalendarApp.xcodeproj/project.pbxproj b/CalendarKitDemo/CalendarApp.xcodeproj/project.pbxproj index fe0c6bce..195f7ca0 100644 --- a/CalendarKitDemo/CalendarApp.xcodeproj/project.pbxproj +++ b/CalendarKitDemo/CalendarApp.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 397054E0253D390E00C33B44 /* MyEventView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 397054DF253D390E00C33B44 /* MyEventView.xib */; }; + 397054E2253D394100C33B44 /* MyEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397054E1253D394100C33B44 /* MyEventView.swift */; }; 75776AF58133F6BBDDCF1CE7 /* Pods_CalendarApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A223D8E39EB8C7A25F4E823A /* Pods_CalendarApp.framework */; }; 791ACF5023FC887D00B2D602 /* TimeChunk+DateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 791ACF4F23FC887D00B2D602 /* TimeChunk+DateComponents.swift */; }; 7924834D23CB7B58003E3D27 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7924834C23CB7B58003E3D27 /* Assets.xcassets */; }; @@ -19,6 +21,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 397054DF253D390E00C33B44 /* MyEventView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MyEventView.xib; sourceTree = ""; }; + 397054E1253D394100C33B44 /* MyEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyEventView.swift; sourceTree = ""; }; 786F0C3439DCA027A1D04664 /* Pods-CalendarApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CalendarApp.release.xcconfig"; path = "Target Support Files/Pods-CalendarApp/Pods-CalendarApp.release.xcconfig"; sourceTree = ""; }; 791ACF4F23FC887D00B2D602 /* TimeChunk+DateComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeChunk+DateComponents.swift"; sourceTree = ""; }; 7924834023CB7B57003E3D27 /* CalendarApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CalendarApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -46,6 +50,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 397054DE253D38F000C33B44 /* CustomEventView */ = { + isa = PBXGroup; + children = ( + 397054DF253D390E00C33B44 /* MyEventView.xib */, + 397054E1253D394100C33B44 /* MyEventView.swift */, + ); + path = CustomEventView; + sourceTree = ""; + }; 57CFFDE58D70A1545DA52F6E /* Pods */ = { isa = PBXGroup; children = ( @@ -76,6 +89,7 @@ 7924834223CB7B57003E3D27 /* CalendarApp */ = { isa = PBXGroup; children = ( + 397054DE253D38F000C33B44 /* CustomEventView */, 7924836523CB7BFE003E3D27 /* AppDelegate.swift */, 791ACF4F23FC887D00B2D602 /* TimeChunk+DateComponents.swift */, 7924836623CB7BFE003E3D27 /* CustomCalendarExampleController.swift */, @@ -159,6 +173,7 @@ buildActionMask = 2147483647; files = ( 7924835023CB7B58003E3D27 /* LaunchScreen.storyboard in Resources */, + 397054E0253D390E00C33B44 /* MyEventView.xib in Resources */, 7924834D23CB7B58003E3D27 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -212,6 +227,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 397054E2253D394100C33B44 /* MyEventView.swift in Sources */, 7924836B23CB7BFE003E3D27 /* AppDelegate.swift in Sources */, 7924836E23CB7BFE003E3D27 /* ExampleNotificationController.swift in Sources */, 7924836C23CB7BFE003E3D27 /* CustomCalendarExampleController.swift in Sources */, @@ -356,13 +372,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = H8S6FURL87; INFOPLIST_FILE = CalendarApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = github.richardtop.CalendarApp; + PRODUCT_BUNDLE_IDENTIFIER = github.richardtop.CalendarApp1123; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -377,13 +394,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = H8S6FURL87; INFOPLIST_FILE = CalendarApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = github.richardtop.CalendarApp; + PRODUCT_BUNDLE_IDENTIFIER = github.richardtop.CalendarApp1123; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/CalendarKitDemo/CalendarApp/CustomCalendarExampleController.swift b/CalendarKitDemo/CalendarApp/CustomCalendarExampleController.swift index 686b43d1..45cabac2 100644 --- a/CalendarKitDemo/CalendarApp/CustomCalendarExampleController.swift +++ b/CalendarKitDemo/CalendarApp/CustomCalendarExampleController.swift @@ -2,8 +2,7 @@ import UIKit import CalendarKit import DateToolsSwift -class CustomCalendarExampleController: DayViewController, DatePickerControllerDelegate { - +class CustomCalendarExampleController: DayViewController, DatePickerControllerDelegate, TimelineViewAppearance { var data = [["Breakfast at Tiffany's", "New York, 5th avenue"], @@ -68,6 +67,7 @@ class CustomCalendarExampleController: DayViewController, DatePickerControllerDe action: #selector(ExampleController.presentDatePicker)) navigationController?.navigationBar.isTranslucent = false dayView.autoScrollToFirstEvent = true + dayView.timelinePagerView.timelineViewAppearance = self reloadData() } @@ -171,6 +171,12 @@ class CustomCalendarExampleController: DayViewController, DatePickerControllerDe baseColor.getHue(&h, saturation: &s, brightness: &b, alpha: &a) return UIColor(hue: h, saturation: s * 0.3, brightness: b, alpha: a) } + + // MARK: TimelineViewAppearance + + func timelineView(_ timeloneView: TimelineView) -> EventView { + MyEventView() + } // MARK: DayViewDelegate diff --git a/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift new file mode 100644 index 00000000..e1ec9612 --- /dev/null +++ b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift @@ -0,0 +1,30 @@ +// +// MyEventView.swift +// CalendarApp +// +// Created by RareScrap on 19.10.2020. +// Copyright © 2020 Richard Topchii. All rights reserved. +// + +import Foundation +import CalendarKit + +class MyEventView: EventView { + + override public init(frame: CGRect) { + super.init(frame: frame) + let v = Bundle.main.loadNibNamed("MyEventView", owner: self, options: nil)?.first as! UIView + self.addSubview(v) + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + let v = Bundle.main.loadNibNamed("MyEventView", owner: self, options: nil)?.first as! UIView + self.addSubview(v) + } + + override func updateWithDescriptor(event: EventDescriptor) { + super.updateWithDescriptor(event: event) + self.color = .black + } +} diff --git a/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib new file mode 100644 index 00000000..340a4b21 --- /dev/null +++ b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Timeline/EventView.swift b/Source/Timeline/EventView.swift index 6df27e60..6ff9cfba 100644 --- a/Source/Timeline/EventView.swift +++ b/Source/Timeline/EventView.swift @@ -42,7 +42,7 @@ open class EventView: UIView { } } - public func updateWithDescriptor(event: EventDescriptor) { + open func updateWithDescriptor(event: EventDescriptor) { if let attributedText = event.attributedText { textView.attributedText = attributedText } else { diff --git a/Source/Timeline/TimelinePagerView.swift b/Source/Timeline/TimelinePagerView.swift index f0a68d92..3f6a1f07 100644 --- a/Source/Timeline/TimelinePagerView.swift +++ b/Source/Timeline/TimelinePagerView.swift @@ -19,6 +19,7 @@ public final class TimelinePagerView: UIView, UIGestureRecognizerDelegate, UIScr public weak var dataSource: EventDataSource? public weak var delegate: TimelinePagerViewDelegate? + public weak var timelineViewAppearance: TimelineViewAppearance? public private(set) var calendar: Calendar = Calendar.autoupdatingCurrent @@ -135,6 +136,7 @@ public final class TimelinePagerView: UIView, UIGestureRecognizerDelegate, UIScr let timeline = controller.timeline timeline.longPressGestureRecognizer.addTarget(self, action: #selector(timelineDidLongPress(_:))) timeline.delegate = self + timeline.appearance = timelineViewAppearance timeline.calendar = calendar timeline.date = date.dateOnly(calendar: calendar) controller.container.delegate = self diff --git a/Source/Timeline/TimelineView.swift b/Source/Timeline/TimelineView.swift index ed29ff57..ec7523b6 100644 --- a/Source/Timeline/TimelineView.swift +++ b/Source/Timeline/TimelineView.swift @@ -9,8 +9,13 @@ public protocol TimelineViewDelegate: AnyObject { func timelineView(_ timelineView: TimelineView, didLongPress event: EventView) } +public protocol TimelineViewAppearance: AnyObject { + func timelineView(_ timeloneView: TimelineView/*, viewFor event: EventDescriptor*/) -> EventView +} + public final class TimelineView: UIView { public weak var delegate: TimelineViewDelegate? + public weak var appearance: TimelineViewAppearance? public var date = Date() { didSet { @@ -53,7 +58,18 @@ public final class TimelineView: UIView { return allDayLayoutAttributes + regularLayoutAttributes } } - private var pool = ReusePool() + + private var storage = [EventView]() + func enqueue(views: [EventView]) { + views.forEach{$0.frame = .zero} + storage.append(contentsOf: views) + } + func dequeue() -> EventView { + guard !storage.isEmpty else { + return appearance?.timelineView(self) ?? EventView() + } + return storage.removeLast() + } public var firstEventYPosition: CGFloat? { let first = regularLayoutAttributes.sorted{$0.frame.origin.y < $1.frame.origin.y}.first @@ -438,10 +454,10 @@ public final class TimelineView: UIView { } private func prepareEventViews() { - pool.enqueue(views: eventViews) + enqueue(views: eventViews) eventViews.removeAll() for _ in regularLayoutAttributes { - let newView = pool.dequeue() + let newView = dequeue() if newView.superview == nil { addSubview(newView) } @@ -450,7 +466,7 @@ public final class TimelineView: UIView { } public func prepareForReuse() { - pool.enqueue(views: eventViews) + enqueue(views: eventViews) eventViews.removeAll() setNeedsDisplay() } From 113bb5ac05b516957047b0b3072a87aa33fe4b87 Mon Sep 17 00:00:00 2001 From: RareScrap Date: Tue, 20 Oct 2020 16:42:44 +0800 Subject: [PATCH 2/4] refactor(view): EventView was divided into EventView (as a base class) and DefaultEventView (current implementation of EventView) --- .../CustomEventView/MyEventView.swift | 22 ++- .../CustomEventView/MyEventView.xib | 59 ++++---- Source/Timeline/EventView.swift | 130 ++++++++++-------- Source/Timeline/TimelinePagerView.swift | 3 +- Source/Timeline/TimelineView.swift | 2 +- 5 files changed, 115 insertions(+), 101 deletions(-) diff --git a/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift index e1ec9612..3e2b2dd1 100644 --- a/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift +++ b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift @@ -11,20 +11,18 @@ import CalendarKit class MyEventView: EventView { - override public init(frame: CGRect) { - super.init(frame: frame) + override func configure() { + super.configure() + let v = Bundle.main.loadNibNamed("MyEventView", owner: self, options: nil)?.first as! UIView self.addSubview(v) - } + + v.translatesAutoresizingMaskIntoConstraints = false + let horizontalConstraint = v.topAnchor.constraint(equalTo: self.topAnchor) + let verticalConstraint = v.bottomAnchor.constraint(equalTo: self.bottomAnchor) + let widthConstraint = v.trailingAnchor.constraint(equalTo: self.trailingAnchor) + let heightConstraint = v.leadingAnchor.constraint(equalTo: self.leadingAnchor) + self.addConstraints([horizontalConstraint, verticalConstraint, widthConstraint, heightConstraint]) - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - let v = Bundle.main.loadNibNamed("MyEventView", owner: self, options: nil)?.first as! UIView - self.addSubview(v) - } - - override func updateWithDescriptor(event: EventDescriptor) { - super.updateWithDescriptor(event: event) - self.color = .black } } diff --git a/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib index 340a4b21..1433b173 100644 --- a/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib +++ b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib @@ -1,5 +1,5 @@ - + @@ -9,36 +9,35 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/Source/Timeline/EventView.swift b/Source/Timeline/EventView.swift index 6ff9cfba..b9af733e 100644 --- a/Source/Timeline/EventView.swift +++ b/Source/Timeline/EventView.swift @@ -3,24 +3,11 @@ import UIKit open class EventView: UIView { public var descriptor: EventDescriptor? - public var color = SystemColors.label - - public var contentHeight: CGFloat { - return textView.frame.height - } - - public lazy var textView: UITextView = { - let view = UITextView() - view.isUserInteractionEnabled = false - view.backgroundColor = .clear - view.isScrollEnabled = false - return view - }() - + /// Resize Handle views showing up when editing the event. /// The top handle has a tag of `0` and the bottom has a tag of `1` - public lazy var eventResizeHandles = [EventResizeHandleView(), EventResizeHandleView()] - + public lazy var eventResizeHandles = [EventResizeHandleView(), EventResizeHandleView()] // TODO: Возможность кастомизации + override public init(frame: CGRect) { super.init(frame: frame) configure() @@ -31,38 +18,58 @@ open class EventView: UIView { configure() } - private func configure() { + open func configure() { clipsToBounds = false - color = tintColor - addSubview(textView) for (idx, handle) in eventResizeHandles.enumerated() { handle.tag = idx addSubview(handle) } } - - open func updateWithDescriptor(event: EventDescriptor) { - if let attributedText = event.attributedText { - textView.attributedText = attributedText - } else { - textView.text = event.text - textView.textColor = event.textColor - textView.font = event.font + + override open func layoutSubviews() { + super.layoutSubviews() + let first = eventResizeHandles.first + let last = eventResizeHandles.last + let radius: CGFloat = 40 + let yPad: CGFloat = -radius / 2 + let width = bounds.width + let height = bounds.height + let size = CGSize(width: radius, height: radius) + first?.frame = CGRect(origin: CGPoint(x: width - radius - layoutMargins.right, y: yPad), + size: size) + last?.frame = CGRect(origin: CGPoint(x: layoutMargins.left, y: height - yPad - radius), + size: size) + } + + /** + Custom implementation of the hitTest method is needed for the tap gesture recognizers + located in the ResizeHandleView to work. + Since the ResizeHandleView could be outside of the EventView's bounds, the touches to the ResizeHandleView + are ignored. + In the custom implementation the method is recursively invoked for all of the subviews, + regardless of their position in relation to the Timeline's bounds. + */ + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for resizeHandle in eventResizeHandles { + if let subSubView = resizeHandle.hitTest(convert(point, to: resizeHandle), with: event) { + return subSubView + } } + return super.hitTest(point, with: event) + } + + open func updateWithDescriptor(event: EventDescriptor) { descriptor = event - backgroundColor = event.backgroundColor - color = event.color eventResizeHandles.forEach{ $0.borderColor = event.color $0.isHidden = event.editedEvent == nil } - drawsShadow = event.editedEvent != nil setNeedsDisplay() setNeedsLayout() } - - public func animateCreation() { + + open func animateCreation() { transform = CGAffineTransform(scaleX: 0.8, y: 0.8) func scaleAnimation() { transform = .identity @@ -75,22 +82,42 @@ open class EventView: UIView { animations: scaleAnimation, completion: nil) } +} - /** - Custom implementation of the hitTest method is needed for the tap gesture recognizers - located in the ResizeHandleView to work. - Since the ResizeHandleView could be outside of the EventView's bounds, the touches to the ResizeHandleView - are ignored. - In the custom implementation the method is recursively invoked for all of the subviews, - regardless of their position in relation to the Timeline's bounds. - */ - public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - for resizeHandle in eventResizeHandles { - if let subSubView = resizeHandle.hitTest(convert(point, to: resizeHandle), with: event) { - return subSubView - } +class DefaultEventView: EventView { + public var color = SystemColors.label + + public var contentHeight: CGFloat { + return textView.frame.height + } + + public lazy var textView: UITextView = { + let view = UITextView() + view.isUserInteractionEnabled = false + view.backgroundColor = .clear + view.isScrollEnabled = false + return view + }() + + override open func configure() { + super.configure() + color = tintColor + addSubview(textView) + } + + override open func updateWithDescriptor(event: EventDescriptor) { + if let attributedText = event.attributedText { + textView.attributedText = attributedText + } else { + textView.text = event.text + textView.textColor = event.textColor + textView.font = event.font } - return super.hitTest(point, with: event) + backgroundColor = event.backgroundColor + color = event.color + drawsShadow = event.editedEvent != nil + + super.updateWithDescriptor(event: event) } override open func draw(_ rect: CGRect) { @@ -123,17 +150,6 @@ open class EventView: UIView { textFrame.size.height += frame.minY; textView.frame = textFrame; } - let first = eventResizeHandles.first - let last = eventResizeHandles.last - let radius: CGFloat = 40 - let yPad: CGFloat = -radius / 2 - let width = bounds.width - let height = bounds.height - let size = CGSize(width: radius, height: radius) - first?.frame = CGRect(origin: CGPoint(x: width - radius - layoutMargins.right, y: yPad), - size: size) - last?.frame = CGRect(origin: CGPoint(x: layoutMargins.left, y: height - yPad - radius), - size: size) if drawsShadow { applySketchShadow(alpha: 0.13, diff --git a/Source/Timeline/TimelinePagerView.swift b/Source/Timeline/TimelinePagerView.swift index 3f6a1f07..61836fb3 100644 --- a/Source/Timeline/TimelinePagerView.swift +++ b/Source/Timeline/TimelinePagerView.swift @@ -201,7 +201,8 @@ public final class TimelinePagerView: UIView, UIGestureRecognizerDelegate, UIScr /// - Parameter event: the EventDescriptor based on which an EventView will be placed on the Timeline /// - Parameter animated: if true, CalendarKit animates event creation public func create(event: EventDescriptor, animated: Bool) { - let eventView = EventView() + // TODO: remove ! `from currentTimeline!.timeline` + let eventView = timelineViewAppearance?.timelineView(currentTimeline!.timeline) ?? DefaultEventView() // TODO: default appearance eventView.updateWithDescriptor(event: event) addSubview(eventView) // layout algo diff --git a/Source/Timeline/TimelineView.swift b/Source/Timeline/TimelineView.swift index ec7523b6..74e1f71a 100644 --- a/Source/Timeline/TimelineView.swift +++ b/Source/Timeline/TimelineView.swift @@ -66,7 +66,7 @@ public final class TimelineView: UIView { } func dequeue() -> EventView { guard !storage.isEmpty else { - return appearance?.timelineView(self) ?? EventView() + return appearance?.timelineView(self) ?? DefaultEventView() // TODO: default appearance } return storage.removeLast() } From d3fb0aae34d3668132ebdd2ab5bf70218a133f1c Mon Sep 17 00:00:00 2001 From: RareScrap Date: Fri, 23 Oct 2020 17:04:01 +0800 Subject: [PATCH 3/4] feat(view): Custom EventView support + It is possible to have several types of EventViews in one timeline --- .../CalendarApp.xcodeproj/project.pbxproj | 4 + .../CustomCalendarExampleController.swift | 36 ++++++--- .../CalendarApp/CustomEventView/MyEvent.swift | 73 +++++++++++++++++++ .../CustomEventView/MyEventView.swift | 4 + .../CustomEventView/MyEventView.xib | 50 +++++++++++++ Source/Header/DaySelector/DateLabel.swift | 2 +- Source/Timeline/EventDescriptor.swift | 10 +-- Source/Timeline/EventView.swift | 2 +- Source/Timeline/TimelinePagerView.swift | 3 +- Source/Timeline/TimelineView.swift | 30 +++++--- 10 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 CalendarKitDemo/CalendarApp/CustomEventView/MyEvent.swift diff --git a/CalendarKitDemo/CalendarApp.xcodeproj/project.pbxproj b/CalendarKitDemo/CalendarApp.xcodeproj/project.pbxproj index 195f7ca0..d40cfdb7 100644 --- a/CalendarKitDemo/CalendarApp.xcodeproj/project.pbxproj +++ b/CalendarKitDemo/CalendarApp.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 391B6FA72541611500A49621 /* MyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391B6FA62541611500A49621 /* MyEvent.swift */; }; 397054E0253D390E00C33B44 /* MyEventView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 397054DF253D390E00C33B44 /* MyEventView.xib */; }; 397054E2253D394100C33B44 /* MyEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397054E1253D394100C33B44 /* MyEventView.swift */; }; 75776AF58133F6BBDDCF1CE7 /* Pods_CalendarApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A223D8E39EB8C7A25F4E823A /* Pods_CalendarApp.framework */; }; @@ -21,6 +22,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 391B6FA62541611500A49621 /* MyEvent.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = MyEvent.swift; sourceTree = ""; }; 397054DF253D390E00C33B44 /* MyEventView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MyEventView.xib; sourceTree = ""; }; 397054E1253D394100C33B44 /* MyEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyEventView.swift; sourceTree = ""; }; 786F0C3439DCA027A1D04664 /* Pods-CalendarApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CalendarApp.release.xcconfig"; path = "Target Support Files/Pods-CalendarApp/Pods-CalendarApp.release.xcconfig"; sourceTree = ""; }; @@ -55,6 +57,7 @@ children = ( 397054DF253D390E00C33B44 /* MyEventView.xib */, 397054E1253D394100C33B44 /* MyEventView.swift */, + 391B6FA62541611500A49621 /* MyEvent.swift */, ); path = CustomEventView; sourceTree = ""; @@ -227,6 +230,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 391B6FA72541611500A49621 /* MyEvent.swift in Sources */, 397054E2253D394100C33B44 /* MyEventView.swift in Sources */, 7924836B23CB7BFE003E3D27 /* AppDelegate.swift in Sources */, 7924836E23CB7BFE003E3D27 /* ExampleNotificationController.swift in Sources */, diff --git a/CalendarKitDemo/CalendarApp/CustomCalendarExampleController.swift b/CalendarKitDemo/CalendarApp/CustomCalendarExampleController.swift index 45cabac2..67d39691 100644 --- a/CalendarKitDemo/CalendarApp/CustomCalendarExampleController.swift +++ b/CalendarKitDemo/CalendarApp/CustomCalendarExampleController.swift @@ -55,6 +55,7 @@ class CustomCalendarExampleController: DayViewController, DatePickerControllerDe override func loadView() { calendar = customCalendar dayView = DayView(calendar: calendar) + dayView.timelinePagerView.timelineViewAppearance = self view = dayView } @@ -125,10 +126,15 @@ class CustomCalendarExampleController: DayViewController, DatePickerControllerDe private func generateEventsForDate(_ date: Date) -> [EventDescriptor] { var workingDate = date.add(TimeChunk.dateComponents(hours: Int(arc4random_uniform(10) + 5))) - var events = [Event]() + var events = [EventDescriptor]() for i in 0...4 { - let event = Event() + let event: EventDescriptor + if (Bool.random()) { + event = Event() + } else { + event = MyEvent() + } let duration = Int(arc4random_uniform(160) + 60) let datePeriod = TimePeriod(beginning: workingDate, chunk: TimeChunk.dateComponents(minutes: duration)) @@ -159,7 +165,7 @@ class CustomCalendarExampleController: DayViewController, DatePickerControllerDe let nextOffset = Int(arc4random_uniform(250) + 40) workingDate = workingDate.add(TimeChunk.dateComponents(minutes: nextOffset)) - event.userInfo = String(i) +// event.userInfo = String(i) } print("Events for \(date)") @@ -174,8 +180,12 @@ class CustomCalendarExampleController: DayViewController, DatePickerControllerDe // MARK: TimelineViewAppearance - func timelineView(_ timeloneView: TimelineView) -> EventView { - MyEventView() + func timelineView(_ timeloneView: TimelineView, viewFor event: EventDescriptor) -> EventView { + if (event is MyEvent) { + return MyEventView() + } else { + return DefaultEventView() + } } // MARK: DayViewDelegate @@ -190,11 +200,12 @@ class CustomCalendarExampleController: DayViewController, DatePickerControllerDe } override func dayViewDidLongPressEventView(_ eventView: EventView) { - guard let descriptor = eventView.descriptor as? Event else { - return - } + let descriptor = eventView.descriptor! +// guard let descriptor = eventView.descriptor as? Event else { // TODO: for what puproses it was here? +// return +// } endEventEditing() - print("Event has been longPressed: \(descriptor) \(String(describing: descriptor.userInfo))") + print("Event has been longPressed: \(descriptor) \(String(describing: descriptor.text/*.userInfo*/))") // TODO: userInfo not works now beginEditing(event: descriptor, animated: true) print(Date()) } @@ -229,7 +240,12 @@ class CustomCalendarExampleController: DayViewController, DatePickerControllerDe private func generateEventNearDate(_ date: Date) -> EventDescriptor { let duration = Int(arc4random_uniform(160) + 60) let startDate = date.subtract(TimeChunk.dateComponents(minutes: Int(CGFloat(duration) / 2))) - let event = Event() + var event: EventDescriptor + if (Bool.random()) { + event = Event() + } else { + event = MyEvent() + } let datePeriod = TimePeriod(beginning: startDate, chunk: TimeChunk.dateComponents(minutes: duration)) event.startDate = datePeriod.beginning! diff --git a/CalendarKitDemo/CalendarApp/CustomEventView/MyEvent.swift b/CalendarKitDemo/CalendarApp/CustomEventView/MyEvent.swift new file mode 100644 index 00000000..2d0c0f35 --- /dev/null +++ b/CalendarKitDemo/CalendarApp/CustomEventView/MyEvent.swift @@ -0,0 +1,73 @@ +// +// MyEvent.swift +// CalendarApp +// +// Created by RareScrap on 22.10.2020. +// Copyright © 2020 Richard Topchii. All rights reserved. +// + +import Foundation +import CalendarKit + +class MyEvent: EventDescriptor { + public var myVar = "asdasd" + public var startDate = Date() + public var endDate = Date() + public var isAllDay = false + public var text = "" + public var attributedText: NSAttributedString? + public var color = SystemColors.systemBlue { + didSet { + updateColors() + } + } + public var backgroundColor = SystemColors.systemBlue.withAlphaComponent(0.3) + public var textColor = SystemColors.label + public var font = UIFont.boldSystemFont(ofSize: 12) + public var userInfo: Any? + public weak var editedEvent: EventDescriptor? { + didSet { + updateColors() + } + } + + public init() {} + + func makeEditable() -> Self { + let cloned = MyEvent() + cloned.startDate = startDate + cloned.endDate = endDate + cloned.isAllDay = isAllDay + cloned.text = text + cloned.attributedText = attributedText + cloned.color = color + cloned.backgroundColor = backgroundColor + cloned.textColor = textColor + cloned.userInfo = userInfo + cloned.editedEvent = self + return cloned as! Self + } + + public func commitEditing() { + guard let edited = editedEvent else {return} + edited.startDate = startDate + edited.endDate = endDate + } + + private func updateColors() { + (editedEvent != nil) ? applyEditingColors() : applyStandardColors() + } + + private func applyStandardColors() { + backgroundColor = color.withAlphaComponent(0.3) + var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + color.getHue(&h, saturation: &s, brightness: &b, alpha: &a) + textColor = UIColor(hue: h, saturation: s, brightness: b * 0.4, alpha: a) + } + + private func applyEditingColors() { + backgroundColor = color + textColor = .white + } + +} diff --git a/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift index 3e2b2dd1..396c4e80 100644 --- a/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift +++ b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.swift @@ -23,6 +23,10 @@ class MyEventView: EventView { let widthConstraint = v.trailingAnchor.constraint(equalTo: self.trailingAnchor) let heightConstraint = v.leadingAnchor.constraint(equalTo: self.leadingAnchor) self.addConstraints([horizontalConstraint, verticalConstraint, widthConstraint, heightConstraint]) + } + override open func updateWithDescriptor(event: EventDescriptor) { + super.updateWithDescriptor(event: event) + subviews[2].backgroundColor = event.color // TODO: should we do this before super? } } diff --git a/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib index 1433b173..bdc232f6 100644 --- a/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib +++ b/CalendarKitDemo/CalendarApp/CustomEventView/MyEventView.xib @@ -9,6 +9,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Header/DaySelector/DateLabel.swift b/Source/Header/DaySelector/DateLabel.swift index 04824b46..9dc7b6cc 100644 --- a/Source/Header/DaySelector/DateLabel.swift +++ b/Source/Header/DaySelector/DateLabel.swift @@ -16,7 +16,7 @@ public final class DateLabel: UILabel, DaySelectorItemProtocol { } } - private var isToday: Bool { + public /*private*/ var isToday: Bool { // TODO: should we return it to private? return calendar.isDateInToday(date) } diff --git a/Source/Timeline/EventDescriptor.swift b/Source/Timeline/EventDescriptor.swift index 79eab3e1..8785bfea 100644 --- a/Source/Timeline/EventDescriptor.swift +++ b/Source/Timeline/EventDescriptor.swift @@ -5,13 +5,13 @@ import UIKit public protocol EventDescriptor: AnyObject { var startDate: Date {get set} var endDate: Date {get set} - var isAllDay: Bool {get} - var text: String {get} + var isAllDay: Bool {get set} // TODO: should we return this to read-only? + var text: String {get set} // TODO: should we return this to read-only? var attributedText: NSAttributedString? {get} var font : UIFont {get} - var color: UIColor {get} - var textColor: UIColor {get} - var backgroundColor: UIColor {get} + var color: UIColor {get set} // TODO: should we return this to read-only? + var textColor: UIColor {get set} // TODO: should we return this to read-only? + var backgroundColor: UIColor {get set} // TODO: should we return this to read-only? var editedEvent: EventDescriptor? {get set} func makeEditable() -> Self func commitEditing() diff --git a/Source/Timeline/EventView.swift b/Source/Timeline/EventView.swift index b9af733e..876015d4 100644 --- a/Source/Timeline/EventView.swift +++ b/Source/Timeline/EventView.swift @@ -84,7 +84,7 @@ open class EventView: UIView { } } -class DefaultEventView: EventView { +public class DefaultEventView: EventView { // TODO: should we return it to private? public var color = SystemColors.label public var contentHeight: CGFloat { diff --git a/Source/Timeline/TimelinePagerView.swift b/Source/Timeline/TimelinePagerView.swift index 61836fb3..eb3e497f 100644 --- a/Source/Timeline/TimelinePagerView.swift +++ b/Source/Timeline/TimelinePagerView.swift @@ -180,6 +180,7 @@ public final class TimelinePagerView: UIView, UIGestureRecognizerDelegate, UIScr let day = TimePeriod(beginning: date, chunk: TimeChunk.dateComponents(days: 1)) let validEvents = events.filter{$0.datePeriod.overlaps(with: day)} + timeline.appearance = self.timelineViewAppearance // TODO: is this a propper place for setting the appearance? timeline.layoutAttributes = validEvents.map(EventLayoutAttributes.init) } @@ -202,7 +203,7 @@ public final class TimelinePagerView: UIView, UIGestureRecognizerDelegate, UIScr /// - Parameter animated: if true, CalendarKit animates event creation public func create(event: EventDescriptor, animated: Bool) { // TODO: remove ! `from currentTimeline!.timeline` - let eventView = timelineViewAppearance?.timelineView(currentTimeline!.timeline) ?? DefaultEventView() // TODO: default appearance + let eventView = timelineViewAppearance?.timelineView(currentTimeline!.timeline, viewFor: event) ?? DefaultEventView() // TODO: default appearance eventView.updateWithDescriptor(event: event) addSubview(eventView) // layout algo diff --git a/Source/Timeline/TimelineView.swift b/Source/Timeline/TimelineView.swift index 74e1f71a..bc5c3119 100644 --- a/Source/Timeline/TimelineView.swift +++ b/Source/Timeline/TimelineView.swift @@ -10,7 +10,7 @@ public protocol TimelineViewDelegate: AnyObject { } public protocol TimelineViewAppearance: AnyObject { - func timelineView(_ timeloneView: TimelineView/*, viewFor event: EventDescriptor*/) -> EventView + func timelineView(_ timeloneView: TimelineView, viewFor event: EventDescriptor) -> EventView } public final class TimelineView: UIView { @@ -59,16 +59,24 @@ public final class TimelineView: UIView { } } - private var storage = [EventView]() + // EventDescriptor.Type -> array of reusable EventViews + private var storage = [String : Array]() func enqueue(views: [EventView]) { - views.forEach{$0.frame = .zero} - storage.append(contentsOf: views) - } - func dequeue() -> EventView { - guard !storage.isEmpty else { - return appearance?.timelineView(self) ?? DefaultEventView() // TODO: default appearance + for v in views { + v.frame = .zero + let descriptorTypeName = String(reflecting: v.descriptor) + if (storage[descriptorTypeName] == nil) { + storage[descriptorTypeName] = [EventView]() } - return storage.removeLast() + storage[descriptorTypeName]!.append(v) + } + } + func dequeue(event: EventDescriptor) -> EventView { + let descriptorTypeName = String(reflecting: event) + guard (storage[descriptorTypeName]?.isEmpty) != nil else { + return appearance?.timelineView(self, viewFor: event) ?? DefaultEventView() + } + return storage[descriptorTypeName]!.removeLast() } public var firstEventYPosition: CGFloat? { @@ -456,8 +464,8 @@ public final class TimelineView: UIView { private func prepareEventViews() { enqueue(views: eventViews) eventViews.removeAll() - for _ in regularLayoutAttributes { - let newView = dequeue() + for attr in regularLayoutAttributes { + let newView = dequeue(event: attr.descriptor) if newView.superview == nil { addSubview(newView) } From f459f05c1833978ccbfdc4afb2fdc7353b700589 Mon Sep 17 00:00:00 2001 From: RareScrap Date: Wed, 28 Oct 2020 12:01:56 +0800 Subject: [PATCH 4/4] bugfix(view): dequeue() no longer delivers views with an zero frame This was because String(reflecting:) was returning an invalid object class name (it included the word "optional"). Now I have to forcibly expand the optional field, but I'm not sure if this is a good solution --- Source/Timeline/EventView.swift | 3 ++- Source/Timeline/TimelineView.swift | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Source/Timeline/EventView.swift b/Source/Timeline/EventView.swift index 876015d4..c6c774e5 100644 --- a/Source/Timeline/EventView.swift +++ b/Source/Timeline/EventView.swift @@ -2,7 +2,8 @@ import UIKit open class EventView: UIView { - public var descriptor: EventDescriptor? + // TODO: I think this must be non optional + public var descriptor: EventDescriptor? // TODO: for what purpose this field is? /// Resize Handle views showing up when editing the event. /// The top handle has a tag of `0` and the bottom has a tag of `1` diff --git a/Source/Timeline/TimelineView.swift b/Source/Timeline/TimelineView.swift index bc5c3119..7fc6aa95 100644 --- a/Source/Timeline/TimelineView.swift +++ b/Source/Timeline/TimelineView.swift @@ -64,7 +64,7 @@ public final class TimelineView: UIView { func enqueue(views: [EventView]) { for v in views { v.frame = .zero - let descriptorTypeName = String(reflecting: v.descriptor) + let descriptorTypeName = String(reflecting: v.descriptor!) // TODO: Is it even possible to nil here? If so, where and what should we do with it? Request a view from the appearance? if (storage[descriptorTypeName] == nil) { storage[descriptorTypeName] = [EventView]() } @@ -73,8 +73,11 @@ public final class TimelineView: UIView { } func dequeue(event: EventDescriptor) -> EventView { let descriptorTypeName = String(reflecting: event) - guard (storage[descriptorTypeName]?.isEmpty) != nil else { - return appearance?.timelineView(self, viewFor: event) ?? DefaultEventView() + guard storage[descriptorTypeName] != nil + && !storage[descriptorTypeName]!.isEmpty else { + let view = appearance?.timelineView(self, viewFor: event) ?? DefaultEventView() + view.updateWithDescriptor(event: event) // because eventDescriptor must be not nil when enqueu() will called for this view // TODO: I think here start a big problem + return view } return storage[descriptorTypeName]!.removeLast() }