diff --git a/VACalendar.podspec b/VACalendar.podspec new file mode 100644 index 0000000..fedcfb2 --- /dev/null +++ b/VACalendar.podspec @@ -0,0 +1,18 @@ +Pod::Spec.new do |s| + s.name = 'VACalendar' + s.version = '0.1.0' + s.summary = 'Custom Calendar for iOS in Swift' + s.swift_version = '4.0' + + s.description = <<-DESC +VACalendar helps create customizable calendar for your app. It also supports vertical and horizontal scroll directions! + DESC + + s.homepage = 'https://github.com/Vodolazkyi/VACalendar' + s.license = 'MIT' + s.author = { 'Anton Vodolazkyi' => 'vodolazky@me.com' } + s.platform = :ios, '10.0' + s.source = { :git => 'https://github.com/Vodolazkyi/VACalendar.git', :tag => s.version.to_s } + s.source_files = 'VACalendar/Sources/*.swift' + +end \ No newline at end of file diff --git a/VACalendar.xcodeproj/project.pbxproj b/VACalendar.xcodeproj/project.pbxproj new file mode 100644 index 0000000..52fc940 --- /dev/null +++ b/VACalendar.xcodeproj/project.pbxproj @@ -0,0 +1,380 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 60086D62204C707A002097E7 /* VACalendar.h in Headers */ = {isa = PBXBuildFile; fileRef = 60086D60204C707A002097E7 /* VACalendar.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 60086D76204C712F002097E7 /* VADay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D69204C712E002097E7 /* VADay.swift */; }; + 60086D77204C712F002097E7 /* VADotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D6A204C712E002097E7 /* VADotView.swift */; }; + 60086D78204C712F002097E7 /* VADayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D6B204C712E002097E7 /* VADayView.swift */; }; + 60086D79204C712F002097E7 /* VACalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D6C204C712E002097E7 /* VACalendarView.swift */; }; + 60086D7A204C712F002097E7 /* VACalendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D6D204C712E002097E7 /* VACalendar.swift */; }; + 60086D7B204C712F002097E7 /* VAWeekDaysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D6E204C712E002097E7 /* VAWeekDaysView.swift */; }; + 60086D7C204C712F002097E7 /* VAWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D6F204C712E002097E7 /* VAWeek.swift */; }; + 60086D7D204C712F002097E7 /* VAMonth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D70204C712E002097E7 /* VAMonth.swift */; }; + 60086D7E204C712F002097E7 /* VAWeekView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D71204C712E002097E7 /* VAWeekView.swift */; }; + 60086D7F204C712F002097E7 /* VAFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D72204C712E002097E7 /* VAFormatters.swift */; }; + 60086D80204C712F002097E7 /* VACalendarMonthDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D73204C712E002097E7 /* VACalendarMonthDelegate.swift */; }; + 60086D81204C712F002097E7 /* VAMonthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D74204C712F002097E7 /* VAMonthView.swift */; }; + 60086D82204C712F002097E7 /* VAMonthHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60086D75204C712F002097E7 /* VAMonthHeaderView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 60086D5D204C707A002097E7 /* VACalendar.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VACalendar.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 60086D60204C707A002097E7 /* VACalendar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VACalendar.h; sourceTree = ""; }; + 60086D61204C707A002097E7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 60086D69204C712E002097E7 /* VADay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VADay.swift; sourceTree = ""; }; + 60086D6A204C712E002097E7 /* VADotView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VADotView.swift; sourceTree = ""; }; + 60086D6B204C712E002097E7 /* VADayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VADayView.swift; sourceTree = ""; }; + 60086D6C204C712E002097E7 /* VACalendarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VACalendarView.swift; sourceTree = ""; }; + 60086D6D204C712E002097E7 /* VACalendar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VACalendar.swift; sourceTree = ""; }; + 60086D6E204C712E002097E7 /* VAWeekDaysView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VAWeekDaysView.swift; sourceTree = ""; }; + 60086D6F204C712E002097E7 /* VAWeek.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VAWeek.swift; sourceTree = ""; }; + 60086D70204C712E002097E7 /* VAMonth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VAMonth.swift; sourceTree = ""; }; + 60086D71204C712E002097E7 /* VAWeekView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VAWeekView.swift; sourceTree = ""; }; + 60086D72204C712E002097E7 /* VAFormatters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VAFormatters.swift; sourceTree = ""; }; + 60086D73204C712E002097E7 /* VACalendarMonthDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VACalendarMonthDelegate.swift; sourceTree = ""; }; + 60086D74204C712F002097E7 /* VAMonthView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VAMonthView.swift; sourceTree = ""; }; + 60086D75204C712F002097E7 /* VAMonthHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VAMonthHeaderView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 60086D59204C707A002097E7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 60086D53204C707A002097E7 = { + isa = PBXGroup; + children = ( + 60086D5F204C707A002097E7 /* VACalendar */, + 60086D5E204C707A002097E7 /* Products */, + ); + sourceTree = ""; + }; + 60086D5E204C707A002097E7 /* Products */ = { + isa = PBXGroup; + children = ( + 60086D5D204C707A002097E7 /* VACalendar.framework */, + ); + name = Products; + sourceTree = ""; + }; + 60086D5F204C707A002097E7 /* VACalendar */ = { + isa = PBXGroup; + children = ( + 60086D60204C707A002097E7 /* VACalendar.h */, + 60086D68204C7117002097E7 /* Sources */, + 60086D61204C707A002097E7 /* Info.plist */, + ); + path = VACalendar; + sourceTree = ""; + }; + 60086D68204C7117002097E7 /* Sources */ = { + isa = PBXGroup; + children = ( + 60086D6D204C712E002097E7 /* VACalendar.swift */, + 60086D73204C712E002097E7 /* VACalendarMonthDelegate.swift */, + 60086D6C204C712E002097E7 /* VACalendarView.swift */, + 60086D69204C712E002097E7 /* VADay.swift */, + 60086D6B204C712E002097E7 /* VADayView.swift */, + 60086D6A204C712E002097E7 /* VADotView.swift */, + 60086D72204C712E002097E7 /* VAFormatters.swift */, + 60086D70204C712E002097E7 /* VAMonth.swift */, + 60086D75204C712F002097E7 /* VAMonthHeaderView.swift */, + 60086D74204C712F002097E7 /* VAMonthView.swift */, + 60086D6F204C712E002097E7 /* VAWeek.swift */, + 60086D6E204C712E002097E7 /* VAWeekDaysView.swift */, + 60086D71204C712E002097E7 /* VAWeekView.swift */, + ); + path = Sources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 60086D5A204C707A002097E7 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 60086D62204C707A002097E7 /* VACalendar.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 60086D5C204C707A002097E7 /* VACalendar */ = { + isa = PBXNativeTarget; + buildConfigurationList = 60086D65204C707A002097E7 /* Build configuration list for PBXNativeTarget "VACalendar" */; + buildPhases = ( + 60086D58204C707A002097E7 /* Sources */, + 60086D59204C707A002097E7 /* Frameworks */, + 60086D5A204C707A002097E7 /* Headers */, + 60086D5B204C707A002097E7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = VACalendar; + productName = VACalendar; + productReference = 60086D5D204C707A002097E7 /* VACalendar.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 60086D54204C707A002097E7 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0920; + TargetAttributes = { + 60086D5C204C707A002097E7 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 0920; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 60086D57204C707A002097E7 /* Build configuration list for PBXProject "VACalendar" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 60086D53204C707A002097E7; + productRefGroup = 60086D5E204C707A002097E7 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 60086D5C204C707A002097E7 /* VACalendar */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 60086D5B204C707A002097E7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 60086D58204C707A002097E7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 60086D76204C712F002097E7 /* VADay.swift in Sources */, + 60086D78204C712F002097E7 /* VADayView.swift in Sources */, + 60086D7B204C712F002097E7 /* VAWeekDaysView.swift in Sources */, + 60086D7E204C712F002097E7 /* VAWeekView.swift in Sources */, + 60086D80204C712F002097E7 /* VACalendarMonthDelegate.swift in Sources */, + 60086D7A204C712F002097E7 /* VACalendar.swift in Sources */, + 60086D7C204C712F002097E7 /* VAWeek.swift in Sources */, + 60086D7D204C712F002097E7 /* VAMonth.swift in Sources */, + 60086D82204C712F002097E7 /* VAMonthHeaderView.swift in Sources */, + 60086D81204C712F002097E7 /* VAMonthView.swift in Sources */, + 60086D7F204C712F002097E7 /* VAFormatters.swift in Sources */, + 60086D77204C712F002097E7 /* VADotView.swift in Sources */, + 60086D79204C712F002097E7 /* VACalendarView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 60086D63204C707A002097E7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 60086D64204C707A002097E7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 60086D66204C707A002097E7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = VACalendar/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = vodolazkyi.VACalendar; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 60086D67204C707A002097E7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = VACalendar/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = vodolazkyi.VACalendar; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 60086D57204C707A002097E7 /* Build configuration list for PBXProject "VACalendar" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 60086D63204C707A002097E7 /* Debug */, + 60086D64204C707A002097E7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 60086D65204C707A002097E7 /* Build configuration list for PBXNativeTarget "VACalendar" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 60086D66204C707A002097E7 /* Debug */, + 60086D67204C707A002097E7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 60086D54204C707A002097E7 /* Project object */; +} diff --git a/VACalendar/Info.plist b/VACalendar/Info.plist new file mode 100644 index 0000000..1007fd9 --- /dev/null +++ b/VACalendar/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/VACalendar/Sources/VACalendar.swift b/VACalendar/Sources/VACalendar.swift new file mode 100644 index 0000000..c63ed3b --- /dev/null +++ b/VACalendar/Sources/VACalendar.swift @@ -0,0 +1,121 @@ +// +// VACalendar.swift +// VACalendar +// +// Created by Anton Vodolazkyi on 20.02.18. +// Copyright © 2018 Vodolazkyi. All rights reserved. +// + +import Foundation + +protocol VACalendarDelegate: class { + func selectedDaysDidUpdate(_ days: [VADay]) +} + +public enum DaysAvailability { + case all + case some([Date]) +} + +public class VACalendar { + + var months = [VAMonth]() + weak var delegate: VACalendarDelegate? + + private let calendar: Calendar + private var daysAvailability: DaysAvailability = .all + + private var selectedDays = [VADay]() { + didSet { + delegate?.selectedDaysDidUpdate(selectedDays) + } + } + + public init( + startDate: Date? = nil, + endDate: Date? = nil, + selectedDate: Date? = Date(), + calendar: Calendar = Calendar.current) { + self.calendar = calendar + + if let selectedDate = selectedDate { + let day = VADay(date: selectedDate, state: .selected, calendar: calendar) + selectedDays = [day] + } + + let startDate = startDate ?? calendar.date(byAdding: .year, value: -1, to: Date())! + let endDate = endDate ?? calendar.date(byAdding: .year, value: 1, to: Date())! + months = generateMonths(from: startDate, endDate: endDate) + } + + func selectDay(_ day: VADay) { + months.first(where: { $0.dateInThisMonth(day.date) })?.setDaySelectionState(day, state:.selected) + selectedDays = [day] + } + + func selectDates(_ dates: [Date]) { + let days = months.flatMap { $0.days(for: dates) } + days.forEach { $0.setSelectionState(.selected) } + selectedDays = days + } + + func setDaysAvailability(_ availability: DaysAvailability) { + daysAvailability = availability + + switch availability { + case .all: + let days = months.flatMap { $0.allDays() } + days.forEach { $0.setState(.available) } + + case .some(let dates): + let allDays = months.flatMap { $0.allDays() } + allDays.forEach { $0.setState(.unavailable) } + let availableDays = dates.flatMap { date in allDays.filter { $0.dateInDay(date) }} + availableDays.forEach { $0.setState(.available) } + } + } + + func setDaySelectionState(_ day: VADay, state: VADayState) { + months.first(where: { $0.dateInThisMonth(day.date) })?.setDaySelectionState(day, state: state) + + if let index = selectedDays.index(of: day) { + selectedDays.remove(at: index) + } else { + selectedDays.append(day) + } + } + + func setSupplementaries(_ data: [(Date, [VADaySupplementary])]) { + let dates = data.map { $0.0 } + let days = months.flatMap { $0.days(for: dates) } + + days.forEach { day in + guard let supplementaries = data.first(where: { day.dateInDay($0.0) })?.1 else { return } + day.set(supplementaries) + } + } + + func deselectAll() { + selectedDays = [] + months.forEach { $0.deselectAll() } + } + + private func generateMonths(from startDate: Date, endDate: Date) -> [VAMonth] { + let startComponents = calendar.dateComponents([.year, .month], from: startDate) + let endComponents = calendar.dateComponents([.year, .month], from: endDate) + var startDate = calendar.date(from: startComponents)! + let endDate = calendar.date(from: endComponents)! + var months = [VAMonth]() + + repeat { + let date = startDate + let month = VAMonth(month: date, calendar: calendar) + month.selectedDays = selectedDays.filter { calendar.isDate($0.date, equalTo: startDate, toGranularity: .month) } + months.append(month) + startDate = calendar.date(byAdding: .month, value: 1, to: date)! + } while !calendar.isDate(startDate, inSameDayAs: endDate) + + return months + } + +} diff --git a/VACalendar/Sources/VACalendarMonthDelegate.swift b/VACalendar/Sources/VACalendarMonthDelegate.swift new file mode 100644 index 0000000..eeb4c0e --- /dev/null +++ b/VACalendar/Sources/VACalendarMonthDelegate.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol VACalendarMonthDelegate: class { + func monthDidChange(_ currentMonth: Date) +} diff --git a/VACalendar/Sources/VACalendarView.swift b/VACalendar/Sources/VACalendarView.swift new file mode 100644 index 0000000..53473d6 --- /dev/null +++ b/VACalendar/Sources/VACalendarView.swift @@ -0,0 +1,246 @@ +// +// VACalendarView.swift +// VACalendar +// +// Created by Anton Vodolazkyi on 20.02.18. +// Copyright © 2018 Vodolazkyi. All rights reserved. +// + +import UIKit + +public enum VASelectionStyle { + case single + case multi +} + +public enum VACalendarScrollDirection { + case horizontal, vertical +} + +@objc +public protocol VACalendarViewDelegate: class { + // use this method for single selection style + @objc optional func selectedDate(_ date: Date) + // use this method for multi selection style + @objc optional func selectedDates(_ dates: [Date]) + +} + +public class VACalendarView: UIScrollView { + + public weak var monthDelegate: VACalendarMonthDelegate? + public weak var dayViewAppearanceDelegate: VADayViewAppearanceDelegate? + public weak var monthViewAppearanceDelegate: VAMonthViewAppearanceDelegate? + public weak var calendarDelegate: VACalendarViewDelegate? + + public var scrollDirection: VACalendarScrollDirection = .vertical + // use this for vertical scroll direction + public var monthVerticalInset: CGFloat = 20 + public var monthVerticalHeaderHeight: CGFloat = 20 + + public var startDate = Date() + public var showDaysOut = true + public var selectionStyle: VASelectionStyle = .single + + private var calculatedWeekHeight: CGFloat = 100 + private let calendar: VACalendar + private var monthViews = [VAMonthView]() + private let maxNumberOfWeek = 6 + private let numberDaysInWeek = 7 + private var weekHeight: CGFloat { + switch scrollDirection { + case .horizontal: + return frame.height / CGFloat(maxNumberOfWeek) + case .vertical: + return frame.width / CGFloat(numberDaysInWeek) + } + } + + public init(frame: CGRect, calendar: VACalendar) { + self.calendar = calendar + + super.init(frame: frame) + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // specify all properties before calling setup() + public func setup() { + delegate = self + calendar.delegate = self + directionSetup() + calculateContentSize() + setupMonths() + scrollToStartDate() + } + + public func nextMonth() { + switch scrollDirection { + case .horizontal: + let x = contentOffset.x + frame.width + guard x < contentSize.width else { return } + + setContentOffset(CGPoint(x: x, y: 0), animated: false) + drawVisibleMonth(with: contentOffset) + case .vertical: break + } + } + + public func previousMonth() { + switch scrollDirection { + case .horizontal: + let x = contentOffset.x - frame.width + guard x >= 0 else { return } + + setContentOffset(CGPoint(x: x, y: 0), animated: false) + drawVisibleMonth(with: contentOffset) + case .vertical: break + } + } + + public func selectDates(_ dates: [Date]) { + calendar.deselectAll() + calendar.selectDates(dates) + } + + public func setAvailableDates(_ availability: DaysAvailability) { + calendar.setDaysAvailability(availability) + } + + public func setSupplementaries(_ data: [(Date, [VADaySupplementary])]) { + calendar.setSupplementaries(data) + } + + private func directionSetup() { + showsVerticalScrollIndicator = false + showsHorizontalScrollIndicator = false + + switch scrollDirection { + case .horizontal: + isPagingEnabled = true + case .vertical: break + } + } + + private func calculateContentSize() { + switch scrollDirection { + case .horizontal: + contentSize.width = frame.width * CGFloat(calendar.months.count) + case .vertical: + let monthsHeight: CGFloat = calendar.months.enumerated().reduce(0) { result, item in + let inset: CGFloat = item.offset == calendar.months.count - 1 ? 0.0 : monthVerticalInset + let height = CGFloat(item.element.numberOfWeeks) * weekHeight + inset + monthVerticalHeaderHeight + return CGFloat(result) + height + } + contentSize.height = monthsHeight + } + } + + private func setupMonths() { + monthViews = calendar.months.map { VAMonthView(month: $0, showDaysOut: showDaysOut, weekHeight: weekHeight) } + + monthViews.enumerated().forEach { index, monthView in + switch scrollDirection { + case .horizontal: + let x = index == 0 ? 0 : monthViews[index - 1].frame.maxX + monthView.frame = CGRect(x: x, y: 0, width: self.frame.width, height: self.frame.height) + case .vertical: + let y = index == 0 ? 0 : monthViews[index - 1].frame.maxY + monthVerticalInset + let height = (CGFloat(monthView.numberOfWeeks) * weekHeight) + monthVerticalHeaderHeight + monthView.frame = CGRect(x: 0, y: y, width: self.frame.width, height: height) + } + self.addSubview(monthView) + } + } + + private func scrollToStartDate() { + let startMonth = monthViews.first(where: { $0.month.dateInThisMonth(startDate) }) + + if let startMonth = startMonth { + setContentOffset(startMonth.frame.origin, animated: false) + } else { + setContentOffset(.zero, animated: false) + } + drawVisibleMonth(with: contentOffset) + } + + private func getMonthView(with offset: CGPoint) -> VAMonthView? { + switch scrollDirection { + case .horizontal: + return monthViews.first(where: { $0.frame.midX >= offset.x }) + case .vertical: + return monthViews.first(where: { $0.frame.midY >= offset.y }) + } + } + + private func drawVisibleMonth(with offset: CGPoint) { + switch scrollDirection { + case .horizontal: + let first: ((offset: Int, element: VAMonthView)) -> Bool = { $0.element.frame.midX >= offset.x } + guard let currentIndex = monthViews.enumerated().first(where: first)?.offset else { return } + + monthViews.enumerated().forEach { index, month in + if index == currentIndex || index + 1 == currentIndex || index - 1 == currentIndex { + month.delegate = self + month.drawWeeksView() + } else { + month.clean() + } + } + + case .vertical: + let first: ((offset: Int, element: VAMonthView)) -> Bool = { $0.element.frame.minY >= offset.y } + guard let currentIndex = monthViews.enumerated().first(where: first)?.offset else { return } + + monthViews.enumerated().forEach { index, month in + if index >= currentIndex - 1 && index <= currentIndex + 1 { + month.delegate = self + month.drawWeeksView() + } else { + month.clean() + } + } + } + } + +} + +extension VACalendarView: UIScrollViewDelegate { + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard let monthView = getMonthView(with: scrollView.contentOffset) else { return } + + monthDelegate?.monthDidChange(monthView.month.date) + drawVisibleMonth(with: scrollView.contentOffset) + } + +} + +extension VACalendarView: VACalendarDelegate { + + func selectedDaysDidUpdate(_ days: [VADay]) { + let dates = days.map { $0.date } + calendarDelegate?.selectedDates?(dates) + } + +} + +extension VACalendarView: VAMonthViewDelegate { + + func dayStateChanged(_ day: VADay, in month: VAMonth) { + switch selectionStyle { + case .single: + guard day.state == .available else { return } + + calendar.deselectAll() + calendar.setDaySelectionState(day, state: .selected) + calendarDelegate?.selectedDate?(day.date) + + case .multi: + calendar.setDaySelectionState(day, state: day.reverseSelectionState) + } + } + +} diff --git a/VACalendar/Sources/VADay.swift b/VACalendar/Sources/VADay.swift new file mode 100644 index 0000000..5c07faa --- /dev/null +++ b/VACalendar/Sources/VADay.swift @@ -0,0 +1,110 @@ +// +// VADay.swift +// VACalendar +// +// Created by Anton Vodolazkyi on 20.02.18. +// Copyright © 2018 Vodolazkyi. All rights reserved. +// + +import UIKit + +@objc +public enum VADayState: Int { + case out, selected, available, unavailable +} + +@objc +public enum VADayShape: Int { + case square, circle +} + +public enum VADaySupplementary: Hashable { + + // 3 dot max + case bottomDots([UIColor]) + + public var hashValue: Int { + switch self { + case .bottomDots: + return 1 + } + } + + public static func ==(lhs: VADaySupplementary, rhs: VADaySupplementary) -> Bool { + return lhs.hashValue == rhs.hashValue + } + +} + +class VADay { + + let date: Date + var stateChanged: ((VADayState) -> Void)? + var supplementariesDidUpdate: (() -> Void)? + let calendar: Calendar + + var reverseSelectionState: VADayState { + return state == .available ? .selected : .available + } + + var isSelected: Bool { + return state == .selected + } + + var isSelectable: Bool { + return state == .selected || state == .available + } + + var dayInMonth: Bool { + return state != .out + } + + var state: VADayState { + didSet { + stateChanged?(state) + } + } + + var supplementaries = Set() { + didSet { + supplementariesDidUpdate?() + } + } + + init(date: Date, state: VADayState, calendar: Calendar) { + self.date = date + self.state = state + self.calendar = calendar + } + + func dateInDay(_ date: Date) -> Bool { + return calendar.isDate(date, equalTo: self.date, toGranularity: .day) + } + + func setSelectionState(_ state: VADayState) { + guard state == reverseSelectionState && isSelectable else { return } + + self.state = state + } + + func setState(_ state: VADayState) { + self.state = state + } + + func set(_ supplementaries: [VADaySupplementary]) { + self.supplementaries = Set(supplementaries) + } + +} + +extension VADay: Comparable { + + static func <(lhs: VADay, rhs: VADay) -> Bool { + return !lhs.dateInDay(rhs.date) + } + + static func ==(lhs: VADay, rhs: VADay) -> Bool { + return lhs.dateInDay(rhs.date) + } + +} diff --git a/VACalendar/Sources/VADayView.swift b/VACalendar/Sources/VADayView.swift new file mode 100644 index 0000000..a326e3e --- /dev/null +++ b/VACalendar/Sources/VADayView.swift @@ -0,0 +1,134 @@ +// +// VADayView.swift +// VACalendar +// +// Created by Anton Vodolazkyi on 20.02.18. +// Copyright © 2018 Vodolazkyi. All rights reserved. +// + +import UIKit + +@objc +public protocol VADayViewAppearanceDelegate: class { + @objc optional func font(for state: VADayState) -> UIFont + @objc optional func textColor(for state: VADayState) -> UIColor + @objc optional func backgroundColor(for state: VADayState) -> UIColor + @objc optional func dotBottomVerticalOffset(for state: VADayState) -> CGFloat + @objc optional func shape() -> VADayShape + // percent of the selected area to be painted + @objc optional func selectedArea() -> CGFloat +} + +protocol VADayViewDelegate: class { + func dayStateChanged(_ day: VADay) +} + +class VADayView: UIView { + + var day: VADay + weak var delegate: VADayViewDelegate? + + weak var dayViewAppearanceDelegate: VADayViewAppearanceDelegate? { + return (superview as? VAWeekView)?.dayViewAppearanceDelegate + } + + private var dotStackView: UIStackView { + let stack = UIStackView() + stack.distribution = .fillEqually + stack.axis = .horizontal + stack.spacing = dotSpacing + return stack + } + + private let dotSpacing: CGFloat = 5 + private let dotSize: CGFloat = 5 + private var supplementaryViews = [UIView]() + private let dateLabel = UILabel() + + init(day: VADay) { + self.day = day + super.init(frame: .zero) + + self.day.stateChanged = { [weak self] state in + self?.setState(state) + } + + self.day.supplementariesDidUpdate = { [weak self] in + self?.updateSupplementaryViews() + } + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapSelect)) + addGestureRecognizer(tapGesture) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupDay() { + let shortestSide = (frame.width < frame.height ? frame.width : frame.height) + let side = shortestSide * (dayViewAppearanceDelegate?.selectedArea?() ?? 0.8) + + dateLabel.font = dayViewAppearanceDelegate?.font?(for: day.state) ?? dateLabel.font + dateLabel.text = VAFormatters.dayFormatter.string(from: day.date) + dateLabel.clipsToBounds = true + dateLabel.textAlignment = .center + dateLabel.frame = CGRect( + x: 0, + y: 0, + width: side, + height: side + ) + dateLabel.center = CGPoint(x: frame.width / 2, y: frame.height / 2) + + if dayViewAppearanceDelegate?.shape?() == .circle { + dateLabel.layer.cornerRadius = side / 2 + } + + setState(day.state) + addSubview(dateLabel) + updateSupplementaryViews() + } + + @objc + private func didTapSelect() { + guard day.state != .out && day.state != .unavailable else { return } + delegate?.dayStateChanged(day) + } + + private func setState(_ state: VADayState) { + dateLabel.textColor = dayViewAppearanceDelegate?.textColor?(for: state) ?? dateLabel.textColor + dateLabel.backgroundColor = dayViewAppearanceDelegate?.backgroundColor?(for: state) ?? dateLabel.backgroundColor + updateSupplementaryViews() + } + + private func updateSupplementaryViews() { + removeAllSupplementaries() + + day.supplementaries.forEach { supplementary in + switch supplementary { + case .bottomDots(let colors): + let stack = dotStackView + + colors.forEach { color in + let dotView = VADotView(size: dotSize, color: color) + stack.addArrangedSubview(dotView) + } + let spaceOffset = CGFloat(colors.count - 1) * dotSpacing + let stackWidth = CGFloat(colors.count) * dotSpacing + spaceOffset + + let verticalOffset = dayViewAppearanceDelegate?.dotBottomVerticalOffset?(for: day.state) ?? 2 + stack.frame = CGRect(x: 0, y: dateLabel.frame.maxY + verticalOffset, width: stackWidth, height: dotSize) + stack.center.x = dateLabel.center.x + addSubview(stack) + supplementaryViews.append(stack) + } + } + } + + private func removeAllSupplementaries() { + supplementaryViews.forEach { $0.removeFromSuperview() } + supplementaryViews = [] + } + +} diff --git a/VACalendar/Sources/VADotView.swift b/VACalendar/Sources/VADotView.swift new file mode 100644 index 0000000..a0b89e8 --- /dev/null +++ b/VACalendar/Sources/VADotView.swift @@ -0,0 +1,26 @@ +// +// VADotView.swift +// VACalendar +// +// Created by Anton Vodolazkyi on 25.02.18. +// Copyright © 2018 Vodolazkyi. All rights reserved. +// + +import UIKit + +class VADotView: UIView { + + init(size: CGFloat, color: UIColor) { + let frame = CGRect(x: 0, y: 0, width: size, height: size) + super.init(frame: frame) + + layer.cornerRadius = frame.height / 2 + clipsToBounds = true + backgroundColor = color + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/VACalendar/Sources/VAFormatters.swift b/VACalendar/Sources/VAFormatters.swift new file mode 100644 index 0000000..abee6d1 --- /dev/null +++ b/VACalendar/Sources/VAFormatters.swift @@ -0,0 +1,25 @@ +// +// VAFormatters.swift +// VACalendar +// +// Created by Anton Vodolazkyi on 26.02.18. +// Copyright © 2018 Vodolazkyi. All rights reserved. +// + +import Foundation + +struct VAFormatters { + + static let dayFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "d" + return formatter + }() + + static let monthFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "MMMM" + return formatter + }() + +} diff --git a/VACalendar/Sources/VAMonth.swift b/VACalendar/Sources/VAMonth.swift new file mode 100644 index 0000000..a6ef66a --- /dev/null +++ b/VACalendar/Sources/VAMonth.swift @@ -0,0 +1,94 @@ +// +// VAMonth.swift +// VACalendar +// +// Created by Anton Vodolazkyi on 20.02.18. +// Copyright © 2018 Vodolazkyi. All rights reserved. +// + +import Foundation + +class VAMonth { + + var weeks = [VAWeek]() + let lastMonthDay: Date + let date: Date + + var isCurrent: Bool { + return calendar.isDate(date, equalTo: Date(), toGranularity: .month) + } + + var numberOfWeeks: Int { + return weeks.count + } + + var selectedDays = [VADay]() { + didSet { + self.weeks = generateWeeks() + } + } + + private let calendar: Calendar + + init(month: Date, calendar: Calendar) { + self.date = month + self.calendar = calendar + self.lastMonthDay = calendar.date(byAdding: DateComponents(month: 1, day: -1), to: date)! + } + + func days(for dates: [Date]) -> [VADay] { + return weeks.flatMap { $0.days(for: dates) } + } + + func allDays() -> [VADay] { + return weeks.flatMap { $0.days }.filter { $0.dayInMonth } + } + + func dateInThisMonth(_ date: Date) -> Bool { + return calendar.isDate(date, equalTo: self.date, toGranularity: .month) + } + + func deselectAll() { + weeks.forEach { $0.deselectAll() } + } + + func setDaySelectionState(_ day: VADay, state: VADayState) { + weeks.first(where: { $0.dateInThisWeek(day.date) })?.setDaySelectionState(day, state: state) + } + + func set(_ day: VADay, supplementaries: [VADaySupplementary]) { + weeks.first(where: { $0.dateInThisWeek(day.date) })?.set(day, supplementaries: supplementaries) + } + + private func generateWeeks() -> [VAWeek] { + var weeks = [VAWeek]() + let components = calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date) + var weekDay = calendar.date(from: components)! + + repeat { + var days = [VADay]() + for index in 0...6 { + guard let dayInWeek = calendar.date(byAdding: .day, value: +index, to: weekDay) else { continue } + let dayState = state(for: dayInWeek) + let day = VADay(date: dayInWeek, state: dayState, calendar: calendar) + days.append(day) + } + let week = VAWeek(days: days, date: weekDay, calendar: calendar) + weeks.append(week) + weekDay = calendar.date(byAdding: .weekOfYear, value: 1, to: weekDay)! + } while calendar.isDate(weekDay, equalTo: lastMonthDay, toGranularity: .month) + + return weeks + } + + private func state(for date: Date) -> VADayState { + if !calendar.isDate(date, equalTo: lastMonthDay, toGranularity: .month) { + return .out + } else if selectedDays.contains(where: { calendar.isDate($0.date , inSameDayAs: date) }) { + return .selected + } else { + return .available + } + } + +} diff --git a/VACalendar/Sources/VAMonthHeaderView.swift b/VACalendar/Sources/VAMonthHeaderView.swift new file mode 100644 index 0000000..aa69e16 --- /dev/null +++ b/VACalendar/Sources/VAMonthHeaderView.swift @@ -0,0 +1,115 @@ +import UIKit + +public protocol VAMonthHeaderViewDelegate: class { + func didTapNextMonth() + func didTapPreviousMonth() +} + +public struct VAMonthHeaderViewAppearance { + + let monthFont: UIFont + let monthTextColor: UIColor + let monthTextWidth: CGFloat + let previousButtonImage: UIImage + let nextButtonImage: UIImage + let dateFormat: String + + init( + monthFont: UIFont = UIFont.systemFont(ofSize: 21), + monthTextColor: UIColor = UIColor.black, + monthTextWidth: CGFloat = 150, + previousButtonImage: UIImage = #imageLiteral(resourceName: "previous_month_button"), + nextButtonImage: UIImage = #imageLiteral(resourceName: "next_month_button"), + dateFormat: String = "MMMM") { + self.monthFont = monthFont + self.monthTextColor = monthTextColor + self.monthTextWidth = monthTextWidth + self.previousButtonImage = previousButtonImage + self.nextButtonImage = nextButtonImage + self.dateFormat = dateFormat + } + +} + +public class VAMonthHeaderView: UIView { + + public var appearance = VAMonthHeaderViewAppearance() { + didSet { + formatter.dateFormat = appearance.dateFormat + setupView() + } + } + + public weak var delegate: VAMonthHeaderViewDelegate? + + private lazy var formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = appearance.dateFormat + return formatter + }() + + private let monthLabel = UILabel() + private let previousButton = UIButton() + private let nextButton = UIButton() + + override public init(frame: CGRect) { + super.init(frame: frame) + + setupView() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + setupView() + } + + public override func layoutSubviews() { + super.layoutSubviews() + + let buttonWidth: CGFloat = 50.0 + monthLabel.frame = CGRect(x: 0, y: 0, width: appearance.monthTextWidth, height: frame.height) + monthLabel.center.x = center.x + previousButton.frame = CGRect(x: monthLabel.frame.minX - buttonWidth, y: 0, width: buttonWidth, height: frame.height) + nextButton.frame = CGRect(x: monthLabel.frame.maxX, y: 0, width: buttonWidth, height: frame.height) + } + + private func setupView() { + subviews.forEach{ $0.removeFromSuperview() } + + backgroundColor = .white + monthLabel.font = appearance.monthFont + monthLabel.textAlignment = .center + + previousButton.setImage(appearance.previousButtonImage, for: .normal) + previousButton.addTarget(self, action: #selector(didTapPrevious(_:)), for: .touchUpInside) + + nextButton.setImage(appearance.nextButtonImage, for: .normal) + nextButton.addTarget(self, action: #selector(didTapNext(_:)), for: .touchUpInside) + + addSubview(monthLabel) + addSubview(previousButton) + addSubview(nextButton) + + layoutSubviews() + } + + @objc + private func didTapNext(_ sender: UIButton) { + delegate?.didTapNextMonth() + } + + @objc + private func didTapPrevious(_ sender: UIButton) { + delegate?.didTapPreviousMonth() + } + +} + +extension VAMonthHeaderView: VACalendarMonthDelegate { + + public func monthDidChange(_ currentMonth: Date) { + monthLabel.text = formatter.string(from: currentMonth) + } + +} diff --git a/VACalendar/Sources/VAMonthView.swift b/VACalendar/Sources/VAMonthView.swift new file mode 100644 index 0000000..4f7b17a --- /dev/null +++ b/VACalendar/Sources/VAMonthView.swift @@ -0,0 +1,131 @@ +// +// VAMonthView.swift +// VACalendar +// +// Created by Anton Vodolazkyi on 20.02.18. +// Copyright © 2018 Vodolazkyi. All rights reserved. +// + +import UIKit + +protocol VAMonthViewDelegate: class { + func dayStateChanged(_ day: VADay, in month: VAMonth) +} + +@objc +public protocol VAMonthViewAppearanceDelegate: class { + @objc optional func leftInset() -> CGFloat + @objc optional func rightInset() -> CGFloat + @objc optional func verticalMonthTitleFont() -> UIFont + @objc optional func verticalMonthTitleColor() -> UIColor + @objc optional func verticalCurrentMonthTitleColor() -> UIColor +} + +class VAMonthView: UIView { + + var numberOfWeeks: Int { + return month.numberOfWeeks + } + + var isDrawn: Bool { + return !weekViews.isEmpty + } + + var scrollDirection: VACalendarScrollDirection { + return (superview as? VACalendarView)?.scrollDirection ?? .horizontal + } + + var monthVerticalHeaderHeight: CGFloat { + return (superview as? VACalendarView)?.monthVerticalHeaderHeight ?? 0.0 + } + + weak var monthViewAppearanceDelegate: VAMonthViewAppearanceDelegate? { + return (superview as? VACalendarView)?.monthViewAppearanceDelegate + } + + weak var dayViewAppearanceDelegate: VADayViewAppearanceDelegate? { + return (superview as? VACalendarView)?.dayViewAppearanceDelegate + } + + weak var delegate: VAMonthViewDelegate? + + let month: VAMonth + + private let showDaysOut: Bool + private var monthLabel: UILabel? + private var weekViews = [VAWeekView]() + private let weekHeight: CGFloat + + init(month: VAMonth, showDaysOut: Bool, weekHeight: CGFloat) { + self.month = month + self.showDaysOut = showDaysOut + self.weekHeight = weekHeight + super.init(frame: .zero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func drawWeeksView() { + guard isDrawn == false else { return } + + let leftInset = monthViewAppearanceDelegate?.leftInset?() ?? 0 + let rightInset = monthViewAppearanceDelegate?.rightInset?() ?? 0 + + if scrollDirection == .vertical { + setupMonthLabel() + } + + self.weekViews = [] + + let initialOffsetY = self.monthLabel?.frame.maxY ?? 0 + let weekViewWidth = self.frame.width - (leftInset + rightInset) + var y: CGFloat = initialOffsetY + + month.weeks.enumerated().forEach { index, week in + let weekView = VAWeekView(week: week, showDaysOut: showDaysOut) + + weekView.frame = CGRect( + x: leftInset, + y: y, + width: weekViewWidth, + height: self.weekHeight + ) + y = weekView.frame.maxY + + weekView.delegate = self + self.weekViews.append(weekView) + self.addSubview(weekView) + weekView.setupDays() + } + } + + func clean() { + monthLabel = nil + weekViews = [] + subviews.forEach { $0.removeFromSuperview() } + } + + private func setupMonthLabel() { + let textColor = month.isCurrent ? monthViewAppearanceDelegate?.verticalCurrentMonthTitleColor?() : + monthViewAppearanceDelegate?.verticalMonthTitleColor?() + + monthLabel = UILabel() + monthLabel?.text = VAFormatters.monthFormatter.string(from: month.date) + monthLabel?.textColor = textColor ?? monthLabel?.textColor + monthLabel?.font = monthViewAppearanceDelegate?.verticalMonthTitleFont?() ?? monthLabel?.font + monthLabel?.sizeToFit() + monthLabel?.center.x = center.x + addSubview(monthLabel ?? UIView()) + } + +} + +extension VAMonthView: VAWeekViewDelegate { + + func dayStateChanged(_ day: VADay, in week: VAWeek) { + delegate?.dayStateChanged(day, in: month) + } + +} diff --git a/VACalendar/Sources/VAWeek.swift b/VACalendar/Sources/VAWeek.swift new file mode 100644 index 0000000..be4b34b --- /dev/null +++ b/VACalendar/Sources/VAWeek.swift @@ -0,0 +1,44 @@ +// +// VAWeek.swift +// VACalendar +// +// Created by Anton Vodolazkyi on 20.02.18. +// Copyright © 2018 Vodolazkyi. All rights reserved. +// + +import Foundation + +class VAWeek { + + var days: [VADay] + let date: Date + + private let calendar: Calendar + + init(days: [VADay], date: Date, calendar: Calendar) { + self.days = days + self.date = date + self.calendar = calendar + } + + func days(for dates: [Date]) -> [VADay] { + return dates.flatMap { date in days.filter { $0.dateInDay(date) && $0.isSelectable }} + } + + func dateInThisWeek(_ date: Date) -> Bool { + return calendar.isDate(date, equalTo: self.date, toGranularity: .weekOfYear) + } + + func deselectAll() { + days.forEach { $0.setSelectionState(.available) } + } + + func setDaySelectionState(_ day: VADay, state: VADayState) { + days.first(where: { $0.dateInDay(day.date) })?.setSelectionState(state) + } + + func set(_ day: VADay, supplementaries: [VADaySupplementary]) { + days.first(where: { $0.dateInDay(day.date) })?.set(supplementaries) + } + +} diff --git a/VACalendar/Sources/VAWeekDaysView.swift b/VACalendar/Sources/VAWeekDaysView.swift new file mode 100644 index 0000000..3a5ff50 --- /dev/null +++ b/VACalendar/Sources/VAWeekDaysView.swift @@ -0,0 +1,127 @@ +import UIKit + +public enum VAWeekDaysSymbolsType { + case short, veryShort + + func names(from calendar: Calendar) -> [String] { + switch self { + case .short: + return calendar.shortWeekdaySymbols + case .veryShort: + return calendar.veryShortWeekdaySymbols + } + } + +} + +public struct VAWeekDaysViewAppearance { + + let symbolsType: VAWeekDaysSymbolsType + let weekDayTextColor: UIColor + let weekDayTextFont: UIFont + let leftInset: CGFloat + let rightInset: CGFloat + let separatorBackgroundColor: UIColor + let calendar: Calendar + + init( + symbolsType: VAWeekDaysSymbolsType = .veryShort, + weekDayTextColor: UIColor = .black, + weekDayTextFont: UIFont = UIFont.systemFont(ofSize: 15), + leftInset: CGFloat = 10.0, + rightInset: CGFloat = 10.0, + separatorBackgroundColor: UIColor = .lightGray, + calendar: Calendar = Calendar.current) { + self.symbolsType = symbolsType + self.weekDayTextColor = weekDayTextColor + self.weekDayTextFont = weekDayTextFont + self.leftInset = leftInset + self.rightInset = rightInset + self.separatorBackgroundColor = separatorBackgroundColor + self.calendar = calendar + } + +} + +public class VAWeekDaysView: UIView { + + private var appearance = VAWeekDaysViewAppearance() { + didSet { + setupView() + } + } + + private let separatorView = UIView() + private var dayLabels = [UILabel]() + + override public init(frame: CGRect) { + super.init(frame: frame) + + setupView() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + setupView() + } + + public override func layoutSubviews() { + super.layoutSubviews() + + let width = frame.width - (appearance.leftInset + appearance.rightInset) + let dayWidth = width / CGFloat(dayLabels.count) + + dayLabels.enumerated().forEach { index, label in + let x = index == 0 ? appearance.leftInset : dayLabels[index - 1].frame.maxX + + label.frame = CGRect( + x: x, + y: 0, + width: dayWidth, + height: self.frame.height + ) + } + + let separatorHeight = 1 / UIScreen.main.scale + let separatorY = frame.height - separatorHeight + separatorView.frame = CGRect( + x: appearance.leftInset, + y: separatorY, + width: width, + height: separatorHeight + ) + } + + private func setupView() { + subviews.forEach { $0.removeFromSuperview() } + dayLabels = [] + + let names = getWeekdayNames() + names.enumerated().forEach { index, name in + let label = UILabel() + label.text = name + label.textAlignment = .center + label.font = appearance.weekDayTextFont + label.textColor = appearance.weekDayTextColor + dayLabels.append(label) + addSubview(label) + } + + separatorView.backgroundColor = appearance.separatorBackgroundColor + addSubview(separatorView) + layoutSubviews() + } + + private func getWeekdayNames() -> [String] { + let symbols = appearance.symbolsType.names(from: appearance.calendar) + + if appearance.calendar.firstWeekday == 1 { + return symbols + } else { + let allDaysWihoutFirst = Array(symbols[appearance.calendar.firstWeekday - 1.. + +//! Project version number for VACalendar. +FOUNDATION_EXPORT double VACalendarVersionNumber; + +//! Project version string for VACalendar. +FOUNDATION_EXPORT const unsigned char VACalendarVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +