add files
This commit is contained in:
parent
30fe5fd5e1
commit
d829c62c64
40 changed files with 2869 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
223
DEVELOPER_MANUAL.md
Normal file
223
DEVELOPER_MANUAL.md
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
# MeteorShowers App Developer Manual
|
||||
|
||||
## Table of Contents
|
||||
1. [Architecture Overview](#architecture-overview)
|
||||
2. [Core Components](#core-components)
|
||||
3. [Dependencies](#dependencies)
|
||||
4. [File Structure](#file-structure)
|
||||
5. [Key Features](#key-features)
|
||||
6. [Notifications System](#notifications-system)
|
||||
7. [Location Services](#location-services)
|
||||
8. [UI Components](#ui-components)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
MeteorShowers is an iOS application built using UIKit that helps users track meteor showers and provides relevant astronomical data. The app follows a traditional MVC (Model-View-Controller) architecture pattern.
|
||||
|
||||
### Design Patterns Used
|
||||
- Singleton (LocationManager, RemindersManager)
|
||||
- Delegate Pattern (LocationManagerDelegate)
|
||||
- Observer Pattern (NotificationCenter for updates)
|
||||
|
||||
## Core Components
|
||||
|
||||
### Models
|
||||
1. **MeteorShower**
|
||||
- Properties: name, datePeak, ZHR (Zenithal Hourly Rate)
|
||||
- Used to represent individual meteor shower events
|
||||
|
||||
2. **LocationManager**
|
||||
- Singleton class managing device location
|
||||
- Handles location permissions and updates
|
||||
- Provides location data to TopView for calculations
|
||||
|
||||
3. **RemindersManager**
|
||||
- Manages local notifications for meteor shower reminders
|
||||
- Handles persistence of reminder settings
|
||||
- Methods:
|
||||
- `saveReminder(for:)`
|
||||
- `removeReminder(for:)`
|
||||
- `hasReminder(for:)`
|
||||
- `restoreAllReminders()`
|
||||
|
||||
4. **AstronomicalCalculations**
|
||||
- Contains moon phase calculation logic
|
||||
- Enum `MoonPhase` for different moon phases
|
||||
- Struct `MoonPhaseCalculation` for calculations
|
||||
|
||||
### ViewControllers
|
||||
|
||||
1. **MainViewController**
|
||||
- Root view controller
|
||||
- Displays list of meteor showers
|
||||
- Background color: .darkMidnight
|
||||
|
||||
2. **ShowerDetailViewController**
|
||||
- Shows detailed information about selected shower
|
||||
- Manages reminder functionality
|
||||
- Key UI elements:
|
||||
- Reminder bell icon
|
||||
- Peak date
|
||||
- ZHR information
|
||||
- Moon phase details
|
||||
|
||||
### Views
|
||||
|
||||
1. **TopView**
|
||||
- Custom view showing astronomical data
|
||||
- Dependencies:
|
||||
- LocationManager
|
||||
- MoonPhaseCalculation
|
||||
- SunRiseSetCalc
|
||||
- Features:
|
||||
- Sunrise/sunset times
|
||||
- Moon phase
|
||||
- Location-based calculations
|
||||
|
||||
2. **MeteorShowerTableViewCell**
|
||||
- Custom cell for meteor shower list
|
||||
- Background: .lightMidnight
|
||||
- Corner radius: 9pt
|
||||
|
||||
## Dependencies
|
||||
|
||||
### External Libraries
|
||||
1. **Alamofire**
|
||||
- Used for network requests
|
||||
- Installation: Swift Package Manager
|
||||
|
||||
2. **SnapKit**
|
||||
- Auto-layout DSL
|
||||
- Used for programmatic constraints
|
||||
|
||||
### Frameworks
|
||||
1. **UIKit**
|
||||
- Main UI framework
|
||||
2. **CoreLocation**
|
||||
- Location services
|
||||
3. **UserNotifications**
|
||||
- Local notifications for reminders
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
MeteorShowers/
|
||||
├── App/
|
||||
│ ├── SceneDelegate.swift
|
||||
│ ├── AppDelegate.swift
|
||||
│ └── Info.plist
|
||||
├── ViewControllers/
|
||||
│ ├── MainViewController.swift
|
||||
│ └── ShowerDetailViewController.swift
|
||||
├── Views/
|
||||
│ ├── TopView.swift
|
||||
│ └── MeteorShowerTableViewCell.swift
|
||||
├── Models/
|
||||
│ ├── MeteorShower.swift
|
||||
│ ├── LocationManager.swift
|
||||
│ └── AstronomicalCalculations.swift
|
||||
├── Managers/
|
||||
│ └── RemindersManager.swift
|
||||
└── Assets.xcassets/
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### Reminder System
|
||||
- Set reminders 3 days before meteor shower peaks
|
||||
- Persistent storage using UserDefaults
|
||||
- Local notifications using UNUserNotificationCenter
|
||||
- Visual feedback with bell icon
|
||||
|
||||
### Location Services
|
||||
- Uses CoreLocation for user position
|
||||
- Permission handling: "When In Use" authorization
|
||||
- Fallback to Moscow coordinates if location unavailable
|
||||
- Used for calculating:
|
||||
- Sunrise/sunset times
|
||||
- Moon phase visibility
|
||||
|
||||
### Astronomical Calculations
|
||||
1. **Sun Calculations**
|
||||
- Sunrise/sunset times based on location
|
||||
- Uses UTC time zone to avoid conversion issues
|
||||
|
||||
2. **Moon Phase**
|
||||
- Calculates current moon phase
|
||||
- Shows illumination percentage
|
||||
- Updates based on current date
|
||||
|
||||
## UI Components
|
||||
|
||||
### Color Scheme
|
||||
- `.darkMidnight`: Main background color
|
||||
- `.lightMidnight`: Secondary background color
|
||||
- `.lightMidnight1`: Navigation title color
|
||||
|
||||
### Typography
|
||||
- Navigation title: System font, large title
|
||||
- Labels: System font, 12pt, weight: thin
|
||||
- Time labels: System font, 12pt, weight: thin
|
||||
|
||||
### Layout
|
||||
- Corner radius: 12pt for main views
|
||||
- Standard padding: 16pt
|
||||
- Icon sizes: 24x24pt
|
||||
|
||||
## Notifications System
|
||||
|
||||
### Local Notifications
|
||||
- Scheduled 3 days before peak
|
||||
- Unique identifier format: "meteorShower-[name]"
|
||||
- Permission requested on first reminder set
|
||||
- Persisted across app restarts
|
||||
|
||||
### Permission Handling
|
||||
```swift
|
||||
NSUserNotificationUsageDescription: "We'll notify you 3 days before meteor shower peaks so you don't miss the show!"
|
||||
```
|
||||
|
||||
## Location Services
|
||||
|
||||
### Permission Handling
|
||||
```swift
|
||||
NSLocationWhenInUseUsageDescription: "We need your location to calculate accurate sunrise and sunset times for your area."
|
||||
```
|
||||
|
||||
### Location Updates
|
||||
- Accuracy level: Best
|
||||
- Updates when:
|
||||
1. App launches
|
||||
2. Returns from background
|
||||
3. Location permission granted
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Memory Management**
|
||||
- Use weak self in closures
|
||||
- Proper deinitialization of observers
|
||||
- Cleanup of notification requests
|
||||
|
||||
2. **Error Handling**
|
||||
- Location fallback mechanism
|
||||
- Graceful degradation of features
|
||||
- User feedback for failures
|
||||
|
||||
3. **Performance**
|
||||
- Reusable cells for list
|
||||
- Efficient astronomical calculations
|
||||
- Proper threading for UI updates
|
||||
|
||||
4. **Code Organization**
|
||||
- Clear separation of concerns
|
||||
- Protocol-oriented design
|
||||
- Extension-based organization
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to this project:
|
||||
1. Follow the existing architecture patterns
|
||||
2. Maintain consistent code style
|
||||
3. Add appropriate documentation
|
||||
4. Test on multiple iOS versions
|
||||
5. Consider backward compatibility
|
||||
412
MeteorShowers.xcodeproj/project.pbxproj
Normal file
412
MeteorShowers.xcodeproj/project.pbxproj
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
57BDB0152D4B641B0013AC04 /* OpenMeteoSdk in Frameworks */ = {isa = PBXBuildFile; productRef = 57BDB0142D4B641B0013AC04 /* OpenMeteoSdk */; };
|
||||
57BDB0182D4BA6FD0013AC04 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 57BDB0172D4BA6FD0013AC04 /* Alamofire */; };
|
||||
57E994872D4652F900912D66 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 57E994862D4652F900912D66 /* SnapKit */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
57E9946B2D46517500912D66 /* MeteorShowers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MeteorShowers.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
57E9947D2D46517600912D66 /* Exceptions for "MeteorShowers" folder in "MeteorShowers" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
App/Info.plist,
|
||||
);
|
||||
target = 57E9946A2D46517500912D66 /* MeteorShowers */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
57E9946D2D46517500912D66 /* MeteorShowers */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
57E9947D2D46517600912D66 /* Exceptions for "MeteorShowers" folder in "MeteorShowers" target */,
|
||||
);
|
||||
path = MeteorShowers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
57E994682D46517500912D66 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
57BDB0182D4BA6FD0013AC04 /* Alamofire in Frameworks */,
|
||||
57BDB0152D4B641B0013AC04 /* OpenMeteoSdk in Frameworks */,
|
||||
57E994872D4652F900912D66 /* SnapKit in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
57E994622D46517500912D66 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
57E9946D2D46517500912D66 /* MeteorShowers */,
|
||||
57E9946C2D46517500912D66 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
57E9946C2D46517500912D66 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
57E9946B2D46517500912D66 /* MeteorShowers.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
57E9946A2D46517500912D66 /* MeteorShowers */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 57E9947E2D46517600912D66 /* Build configuration list for PBXNativeTarget "MeteorShowers" */;
|
||||
buildPhases = (
|
||||
57E994672D46517500912D66 /* Sources */,
|
||||
57E994682D46517500912D66 /* Frameworks */,
|
||||
57E994692D46517500912D66 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
57E9946D2D46517500912D66 /* MeteorShowers */,
|
||||
);
|
||||
name = MeteorShowers;
|
||||
packageProductDependencies = (
|
||||
57E994862D4652F900912D66 /* SnapKit */,
|
||||
57BDB0142D4B641B0013AC04 /* OpenMeteoSdk */,
|
||||
57BDB0172D4BA6FD0013AC04 /* Alamofire */,
|
||||
);
|
||||
productName = MeteorShowers;
|
||||
productReference = 57E9946B2D46517500912D66 /* MeteorShowers.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
57E994632D46517500912D66 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1610;
|
||||
LastUpgradeCheck = 1610;
|
||||
TargetAttributes = {
|
||||
57E9946A2D46517500912D66 = {
|
||||
CreatedOnToolsVersion = 16.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 57E994662D46517500912D66 /* Build configuration list for PBXProject "MeteorShowers" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 57E994622D46517500912D66;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
57E994852D4652F900912D66 /* XCRemoteSwiftPackageReference "SnapKit" */,
|
||||
57BDB0132D4B641B0013AC04 /* XCRemoteSwiftPackageReference "sdk" */,
|
||||
57BDB0162D4BA6FD0013AC04 /* XCRemoteSwiftPackageReference "Alamofire" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 57E9946C2D46517500912D66 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
57E9946A2D46517500912D66 /* MeteorShowers */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
57E994692D46517500912D66 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
57E994672D46517500912D66 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
57E9947F2D46517600912D66 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = MF7QC2JX3N;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = MeteorShowers/App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Meteor Showers";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.swiftvibes.MeteorShowers;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
57E994802D46517600912D66 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = MF7QC2JX3N;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = MeteorShowers/App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Meteor Showers";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ru.swiftvibes.MeteorShowers;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
57E994812D46517600912D66 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
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;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
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 = 18.1;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
57E994822D46517600912D66 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
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;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
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 = 18.1;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
57E994662D46517500912D66 /* Build configuration list for PBXProject "MeteorShowers" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
57E994812D46517600912D66 /* Debug */,
|
||||
57E994822D46517600912D66 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
57E9947E2D46517600912D66 /* Build configuration list for PBXNativeTarget "MeteorShowers" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
57E9947F2D46517600912D66 /* Debug */,
|
||||
57E994802D46517600912D66 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
57BDB0132D4B641B0013AC04 /* XCRemoteSwiftPackageReference "sdk" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/open-meteo/sdk.git";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
57BDB0162D4BA6FD0013AC04 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Alamofire/Alamofire.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.10.2;
|
||||
};
|
||||
};
|
||||
57E994852D4652F900912D66 /* XCRemoteSwiftPackageReference "SnapKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SnapKit/SnapKit.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.7.1;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
57BDB0142D4B641B0013AC04 /* OpenMeteoSdk */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 57BDB0132D4B641B0013AC04 /* XCRemoteSwiftPackageReference "sdk" */;
|
||||
productName = OpenMeteoSdk;
|
||||
};
|
||||
57BDB0172D4BA6FD0013AC04 /* Alamofire */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 57BDB0162D4BA6FD0013AC04 /* XCRemoteSwiftPackageReference "Alamofire" */;
|
||||
productName = Alamofire;
|
||||
};
|
||||
57E994862D4652F900912D66 /* SnapKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 57E994852D4652F900912D66 /* XCRemoteSwiftPackageReference "SnapKit" */;
|
||||
productName = SnapKit;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 57E994632D46517500912D66 /* Project object */;
|
||||
}
|
||||
7
MeteorShowers.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
MeteorShowers.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"originHash" : "48a78d05fda71a2681982bb7ce0483c4c94e8a750482ff401b9989bd548e3ab4",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "alamofire",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||
"state" : {
|
||||
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
|
||||
"version" : "5.10.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "flatbuffers",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/flatbuffers.git",
|
||||
"state" : {
|
||||
"revision" : "595bf0007ab1929570c7671f091313c8fc20644e",
|
||||
"version" : "24.3.25"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/open-meteo/sdk.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "df6da44757b014199275bff3b03df7b5d9876d94"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "snapkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SnapKit/SnapKit.git",
|
||||
"state" : {
|
||||
"revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4",
|
||||
"version" : "5.7.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "57E9946A2D46517500912D66"
|
||||
BuildableName = "MeteorShowers.app"
|
||||
BlueprintName = "MeteorShowers"
|
||||
ReferencedContainer = "container:MeteorShowers.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "57E9946A2D46517500912D66"
|
||||
BuildableName = "MeteorShowers.app"
|
||||
BlueprintName = "MeteorShowers"
|
||||
ReferencedContainer = "container:MeteorShowers.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "CORESVG_VERBOSE"
|
||||
value = "1"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "57E9946A2D46517500912D66"
|
||||
BuildableName = "MeteorShowers.app"
|
||||
BlueprintName = "MeteorShowers"
|
||||
ReferencedContainer = "container:MeteorShowers.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>MeteorShowers.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>SnapKitPlayground (Playground).xcscheme</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>57E9946A2D46517500912D66</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
MeteorShowers/.DS_Store
vendored
Normal file
BIN
MeteorShowers/.DS_Store
vendored
Normal file
Binary file not shown.
36
MeteorShowers/App/AppDelegate.swift
Normal file
36
MeteorShowers/App/AppDelegate.swift
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// AppDelegate.swift
|
||||
// MeteorShowers
|
||||
//
|
||||
// Created by Vic on 26.01.2025.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
27
MeteorShowers/App/Info.plist
Normal file
27
MeteorShowers/App/Info.plist
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>We need your location to calculate accurate sunrise and sunset times for your area.</string>
|
||||
<key>NSUserNotificationUsageDescription</key>
|
||||
<string>We'll notify you 3 days before meteor shower peaks so you don't miss the show!</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
77
MeteorShowers/App/SceneDelegate.swift
Normal file
77
MeteorShowers/App/SceneDelegate.swift
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// SceneDelegate.swift
|
||||
// MeteorShowers
|
||||
//
|
||||
// Created by Vic on 26.01.2025.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
|
||||
// MARK: - since we don't use storyboards - this must be implemented
|
||||
guard let customWindowScene = (scene as? UIWindowScene) else { return }
|
||||
|
||||
// Restore reminders when app launches
|
||||
RemindersManager.shared.restoreAllReminders()
|
||||
|
||||
// custom window since we don't use Storyboards
|
||||
window = UIWindow(frame: customWindowScene.coordinateSpace.bounds)
|
||||
window?.windowScene = customWindowScene
|
||||
|
||||
// root VC
|
||||
let mainViewController = MainViewController()
|
||||
|
||||
// assigning root vc to nav
|
||||
let navigationController = UINavigationController(rootViewController: mainViewController)
|
||||
window?.rootViewController = navigationController
|
||||
|
||||
// configuring nav title
|
||||
let titleColor: UIColor = .gray
|
||||
navigationController.navigationBar.largeTitleTextAttributes = [
|
||||
.foregroundColor: titleColor
|
||||
]
|
||||
navigationController.navigationBar.titleTextAttributes = [
|
||||
.foregroundColor: titleColor
|
||||
]
|
||||
navigationController.navigationBar.prefersLargeTitles = true
|
||||
|
||||
// setting title to nav via mainVC
|
||||
mainViewController.title = "Meteor Showers"
|
||||
window?.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
// Called as the scene is being released by the system.
|
||||
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
// Called when the scene has moved from an inactive state to an active state.
|
||||
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
// Called when the scene will move from an active state to an inactive state.
|
||||
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the background to the foreground.
|
||||
// Use this method to undo the changes made on entering the background.
|
||||
}
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the foreground to the background.
|
||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||
// to restore the scene back to its current state.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon_main.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
MeteorShowers/Assets.xcassets/AppIcon.appiconset/icon_main.png
Normal file
BIN
MeteorShowers/Assets.xcassets/AppIcon.appiconset/icon_main.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
6
MeteorShowers/Assets.xcassets/Contents.json
Normal file
6
MeteorShowers/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
MeteorShowers/Assets.xcassets/bg_m.imageset/Contents.json
vendored
Normal file
21
MeteorShowers/Assets.xcassets/bg_m.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bg_m.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
MeteorShowers/Assets.xcassets/bg_m.imageset/bg_m.png
vendored
Normal file
BIN
MeteorShowers/Assets.xcassets/bg_m.imageset/bg_m.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.325",
|
||||
"green" : "0.157",
|
||||
"red" : "0.090"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.651",
|
||||
"green" : "0.394",
|
||||
"red" : "0.240"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.366",
|
||||
"green" : "0.195",
|
||||
"red" : "0.117"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.540",
|
||||
"green" : "0.311",
|
||||
"red" : "0.191"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
MeteorShowers/Assets.xcassets/mock.imageset/Contents.json
vendored
Normal file
21
MeteorShowers/Assets.xcassets/mock.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "mock_pix.jpg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
MeteorShowers/Assets.xcassets/mock.imageset/mock_pix.jpg
vendored
Normal file
BIN
MeteorShowers/Assets.xcassets/mock.imageset/mock_pix.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
44
MeteorShowers/Base.lproj/LaunchScreen.storyboard
Normal file
44
MeteorShowers/Base.lproj/LaunchScreen.storyboard
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bg_m" translatesAutoresizingMaskIntoConstraints="NO" id="Sz2-Em-jJh">
|
||||
<rect key="frame" x="-169" y="-189" width="1130" height="1207"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
<color key="backgroundColor" systemColor="systemTealColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="Sz2-Em-jJh" secondAttribute="trailing" constant="-568" id="FIX-0T-Jqg"/>
|
||||
<constraint firstItem="Sz2-Em-jJh" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" constant="-189" id="LeO-LK-k6P"/>
|
||||
<constraint firstItem="Sz2-Em-jJh" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" constant="-169" id="cku-nT-EkD"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Sz2-Em-jJh" secondAttribute="bottom" constant="-166" id="moU-Wv-Ms1"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="52.671755725190835" y="374.64788732394368"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="bg_m" width="1508" height="1518"/>
|
||||
<systemColor name="systemTealColor">
|
||||
<color red="0.18823529410000001" green="0.69019607839999997" blue="0.78039215689999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
219
MeteorShowers/Cells/MeteorShowerTableViewCell.swift
Normal file
219
MeteorShowers/Cells/MeteorShowerTableViewCell.swift
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
//
|
||||
// MeteorShowerTableViewCell.swift
|
||||
// MeteorShowers
|
||||
//
|
||||
// Created by Vic on 27.01.2025.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class MeteorShowerTableViewCell: UITableViewCell {
|
||||
|
||||
// MARK: - UI Elements
|
||||
private lazy var containerView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .lightMidnight
|
||||
view.layer.cornerRadius = 9
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
// MARK: - Stacks
|
||||
private lazy var mainStackView: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.alignment = .fill
|
||||
stack.spacing = 8
|
||||
stack.distribution = .fillProportionally
|
||||
|
||||
return stack
|
||||
}()
|
||||
|
||||
// used for info about shower
|
||||
private lazy var infoStackView: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.spacing = 1
|
||||
stack.alignment = .fill
|
||||
stack.distribution = .fillEqually
|
||||
|
||||
return stack
|
||||
}()
|
||||
|
||||
// dates inside info, inside this stack three labels
|
||||
private lazy var datesStackView: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 8
|
||||
stack.alignment = .fill
|
||||
stack.distribution = .fillEqually
|
||||
|
||||
return stack
|
||||
}()
|
||||
|
||||
// used for moon phase, img+zhr label
|
||||
private lazy var moonAndOriginStackView: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .fill
|
||||
stack.distribution = .fillEqually
|
||||
|
||||
return stack
|
||||
}()
|
||||
|
||||
private lazy var moonStackView: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.alignment = .leading
|
||||
stack.spacing = 8
|
||||
stack.distribution = .equalCentering
|
||||
|
||||
return stack
|
||||
}()
|
||||
|
||||
private lazy var originStackView: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .leading
|
||||
stack.distribution = .fillEqually
|
||||
|
||||
return stack
|
||||
}()
|
||||
|
||||
// MARK: - Labels
|
||||
// title
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
label.textColor = .lightGray
|
||||
label.text = "Meteor Shower"
|
||||
return label
|
||||
}()
|
||||
|
||||
// dates labels
|
||||
private lazy var datePeakLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 12, weight: .heavy)
|
||||
label.textColor = .lightGray
|
||||
label.text = "Peak: TBD"
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var dateBeginLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 11)
|
||||
label.textColor = .gray
|
||||
label.text = "Begins: TBD"
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var dateEndLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 12)
|
||||
label.textColor = .gray
|
||||
label.text = "Ends: TBD"
|
||||
return label
|
||||
}()
|
||||
|
||||
// moon image
|
||||
private lazy var moonIconImageView: UIImageView = {
|
||||
let image = UIImageView(image: UIImage(systemName: "moonphase.waxing.gibbous"))
|
||||
image.tintColor = .lightGray
|
||||
return image
|
||||
}()
|
||||
|
||||
// ZHR label
|
||||
private lazy var ZHRLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 14, weight: .bold)
|
||||
label.textAlignment = .left
|
||||
label.textColor = .gray
|
||||
label.text = ""
|
||||
return label
|
||||
}()
|
||||
|
||||
// Shower origin
|
||||
private lazy var originLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .gray
|
||||
label.font = .systemFont(ofSize: 11, weight: .heavy)
|
||||
label.numberOfLines = 2
|
||||
label.textColor = .lightGray
|
||||
label.text = "Comet"
|
||||
|
||||
return label
|
||||
}()
|
||||
|
||||
// Add moonPhaseCalculator property
|
||||
private let moonPhaseCalculator = MoonPhaseCalculation()
|
||||
|
||||
// MARK: - Initialization
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
// MARK: - Setup UI
|
||||
private func setupUI() {
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
|
||||
// filling screen with contents
|
||||
contentView.addSubview(containerView)
|
||||
containerView.addSubview(mainStackView)
|
||||
|
||||
mainStackView.addArrangedSubview(titleLabel)
|
||||
mainStackView.addArrangedSubview(infoStackView)
|
||||
|
||||
infoStackView.addArrangedSubview(datesStackView)
|
||||
infoStackView.addArrangedSubview(moonAndOriginStackView)
|
||||
|
||||
moonAndOriginStackView.addArrangedSubview(moonStackView)
|
||||
moonAndOriginStackView.addArrangedSubview(originStackView)
|
||||
|
||||
moonStackView.addArrangedSubview(moonIconImageView)
|
||||
moonStackView.addArrangedSubview(ZHRLabel)
|
||||
|
||||
datesStackView.addArrangedSubview(dateBeginLabel)
|
||||
datesStackView.addArrangedSubview(datePeakLabel)
|
||||
datesStackView.addArrangedSubview(dateEndLabel)
|
||||
|
||||
originStackView.addArrangedSubview(originLabel)
|
||||
|
||||
// main container
|
||||
containerView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
|
||||
}
|
||||
|
||||
// main stackview in main container
|
||||
mainStackView.snp.makeConstraints { make in
|
||||
make.leading.top.equalToSuperview().offset(12)
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configure cell
|
||||
func configure(with shower: MeteorShower) {
|
||||
titleLabel.text = shower.name
|
||||
dateBeginLabel.text = "Begins: \(shower.formattedBeginDate)"
|
||||
datePeakLabel.text = "Peak: \(shower.formattedPeakDate)"
|
||||
dateEndLabel.text = "Ends: \(shower.formattedEndDate)"
|
||||
ZHRLabel.text = "\(shower.formattedZHR)"
|
||||
originLabel.text = """
|
||||
Parent body:
|
||||
\(shower.parentBodyLabel)
|
||||
"""
|
||||
|
||||
// Calculate moon phase for shower's peak date
|
||||
let moonPhase = moonPhaseCalculator.getMoonPhase(date: shower.datePeak)
|
||||
if let phase = MoonPhase(rawValue: moonPhase.phase) {
|
||||
moonIconImageView.image = phase.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
25
MeteorShowers/Extensions/UIView+Ext.swift
Normal file
25
MeteorShowers/Extensions/UIView+Ext.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// UIView+Ext.swift
|
||||
// MeteorShowers
|
||||
//
|
||||
// Created by Vic on 26.01.2025.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
extension UIView {
|
||||
|
||||
func snapKitPin(to superView: UIView) {
|
||||
self.snp.makeConstraints({
|
||||
$0.edges.equalToSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension UIView {
|
||||
class func fromNib<T: UIView>() -> T {
|
||||
return Bundle(for: T.self).loadNibNamed(String(describing: T.self), owner: nil, options: nil)![0] as! T
|
||||
}
|
||||
}
|
||||
158
MeteorShowers/Models/AstronomicalCalculations.swift
Normal file
158
MeteorShowers/Models/AstronomicalCalculations.swift
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
//
|
||||
// MoonPhase.swift
|
||||
// MeteorShowers
|
||||
//
|
||||
// Created by Vic on 28.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
import UIKit
|
||||
|
||||
enum MoonPhase: String, CaseIterable {
|
||||
case newMoon = "New Moon"
|
||||
case waxingCrescent = "Waxing Crescent"
|
||||
case firstQuarter = "First Quarter"
|
||||
case waxingGibbous = "Waxing Gibbous"
|
||||
case fullMoon = "Full Moon"
|
||||
case waningGibbous = "Waning Gibbous"
|
||||
case lastQuarter = "Last Quarter"
|
||||
case waningCrescent = "Waning Crescent"
|
||||
|
||||
// method gets icon from SF Symbols
|
||||
var iconName: String {
|
||||
switch self {
|
||||
case .newMoon: return "moonphase.new.moon"
|
||||
case .waxingCrescent: return "moonphase.waxing.crescent"
|
||||
case .firstQuarter: return "moonphase.first.quarter"
|
||||
case .waxingGibbous: return "moonphase.waxing.gibbous"
|
||||
case .fullMoon: return "moonphase.full.moon"
|
||||
case .waningGibbous: return "moonphase.waning.gibbous"
|
||||
case .lastQuarter: return "moonphase.last.quarter"
|
||||
case .waningCrescent: return "moonphase.waning.crescent"
|
||||
}
|
||||
}
|
||||
|
||||
// method getting icon and convert it to UIImage
|
||||
var icon: UIImage? {
|
||||
return UIImage(systemName: self.iconName)
|
||||
}
|
||||
}
|
||||
|
||||
struct MoonPhaseCalculation {
|
||||
// Known new moon date for better accuracy
|
||||
private let knownNewMoon: Date = {
|
||||
var components = DateComponents()
|
||||
components.year = 2024
|
||||
components.month = 12
|
||||
components.day = 31
|
||||
components.hour = 3
|
||||
components.minute = 27
|
||||
components.second = 0
|
||||
return Calendar(identifier: .gregorian).date(from: components)!
|
||||
}()
|
||||
|
||||
private let synodicMonth: Double = 29.53058867 // average duration of synodic month
|
||||
|
||||
func getMoonPhase(date: Date = Date()) -> (phase: String, illumination: Double, age: Double) {
|
||||
let elapsedTime = date.timeIntervalSince(knownNewMoon) / 86400.0 // convert to days
|
||||
let moonAge = elapsedTime.truncatingRemainder(dividingBy: synodicMonth)
|
||||
let normalizedAge = moonAge < 0 ? moonAge + synodicMonth : moonAge
|
||||
|
||||
// Calculate illumination using the normalized age
|
||||
let angleInRadians = 2 * .pi * normalizedAge / synodicMonth
|
||||
let illumination = ((1 - cos(angleInRadians)) / 2) * 100
|
||||
|
||||
let phase: String
|
||||
switch normalizedAge {
|
||||
case 0..<1.84:
|
||||
phase = MoonPhase.newMoon.rawValue
|
||||
case 1.84..<5.53:
|
||||
phase = MoonPhase.waxingCrescent.rawValue
|
||||
case 5.53..<9.22:
|
||||
phase = MoonPhase.firstQuarter.rawValue
|
||||
case 9.22..<12.91:
|
||||
phase = MoonPhase.waxingGibbous.rawValue
|
||||
case 12.91..<16.61:
|
||||
phase = MoonPhase.fullMoon.rawValue
|
||||
case 16.61..<20.30:
|
||||
phase = MoonPhase.waningGibbous.rawValue
|
||||
case 20.30..<23.99:
|
||||
phase = MoonPhase.lastQuarter.rawValue
|
||||
default:
|
||||
phase = MoonPhase.waningCrescent.rawValue
|
||||
}
|
||||
|
||||
return (phase: phase, illumination: illumination, age: normalizedAge)
|
||||
}
|
||||
}
|
||||
|
||||
struct SunRiseSetCalc {
|
||||
func calculateSunriseSunset(for date: Date = Date(), at location: CLLocation) -> (sunrise: Date?, sunset: Date?) {
|
||||
// Get the start of day in local timezone
|
||||
let calendar = Calendar.current
|
||||
let startOfDay = calendar.startOfDay(for: date)
|
||||
|
||||
// Constants
|
||||
let latitude = location.coordinate.latitude
|
||||
let longitude = location.coordinate.longitude
|
||||
let dayOfYear = calendar.ordinality(of: .day, in: .year, for: date) ?? 1
|
||||
|
||||
// Calculate solar declination
|
||||
let gamma = 2.0 * .pi / 365.0 * Double(dayOfYear - 1)
|
||||
let declination = 0.006918 - 0.399912 * cos(gamma) + 0.070257 * sin(gamma) - 0.006758 * cos(2 * gamma) + 0.000907 * sin(2 * gamma) - 0.002697 * cos(3 * gamma) + 0.00148 * sin(3 * gamma)
|
||||
|
||||
// Calculate time offset
|
||||
let eqTime = 229.18 * (0.000075 + 0.001868 * cos(gamma) - 0.032077 * sin(gamma) - 0.014615 * cos(2 * gamma) - 0.040849 * sin(2 * gamma))
|
||||
|
||||
// Calculate solar noon (in minutes from midnight UTC)
|
||||
let solarNoon = (720 - 4.0 * longitude - eqTime) / 1440.0 * 24.0 // Convert to hours
|
||||
|
||||
// Calculate hour angle
|
||||
let ha = acos(cos(90.833 * .pi / 180) / (cos(latitude * .pi / 180) * cos(declination)) - tan(latitude * .pi / 180) * tan(declination))
|
||||
let haHours = ha * 180.0 / .pi / 15.0
|
||||
|
||||
// Calculate sunrise and sunset times (in hours UTC)
|
||||
let sunriseHour = (solarNoon - haHours)
|
||||
let sunsetHour = (solarNoon + haHours)
|
||||
|
||||
// Convert hours to date components
|
||||
var riseComponents = calendar.dateComponents([.year, .month, .day], from: startOfDay)
|
||||
riseComponents.hour = Int(floor(sunriseHour))
|
||||
riseComponents.minute = Int((sunriseHour.truncatingRemainder(dividingBy: 1.0) * 60.0).rounded())
|
||||
|
||||
var setComponents = calendar.dateComponents([.year, .month, .day], from: startOfDay)
|
||||
setComponents.hour = Int(floor(sunsetHour))
|
||||
setComponents.minute = Int((sunsetHour.truncatingRemainder(dividingBy: 1.0) * 60.0).rounded())
|
||||
|
||||
// Create UTC dates
|
||||
var utcCalendar = Calendar(identifier: .gregorian)
|
||||
utcCalendar.timeZone = TimeZone(secondsFromGMT: 0)!
|
||||
|
||||
guard let sunriseUTC = utcCalendar.date(from: riseComponents),
|
||||
let sunsetUTC = utcCalendar.date(from: setComponents) else {
|
||||
return (nil, nil)
|
||||
}
|
||||
|
||||
// Convert to local time
|
||||
let localSunrise = calendar.date(byAdding: .second, value: TimeZone.current.secondsFromGMT(), to: sunriseUTC)
|
||||
let localSunset = calendar.date(byAdding: .second, value: TimeZone.current.secondsFromGMT(), to: sunsetUTC)
|
||||
|
||||
print("""
|
||||
🌅 Debug Sun Calculation:
|
||||
Location: \(latitude)°N, \(longitude)°E
|
||||
Day of Year: \(dayOfYear)
|
||||
Solar Declination: \(declination * 180.0 / .pi)°
|
||||
Equation of Time: \(eqTime) minutes
|
||||
Solar Noon (UTC): \(solarNoon) hours
|
||||
Hour Angle: \(haHours) hours
|
||||
Sunrise Hour (UTC): \(sunriseHour) hours
|
||||
Sunset Hour (UTC): \(sunsetHour) hours
|
||||
Local Timezone Offset: \(TimeZone.current.secondsFromGMT()/3600) hours
|
||||
Final Local Sunrise: \(localSunrise ?? Date())
|
||||
Final Local Sunset: \(localSunset ?? Date())
|
||||
""")
|
||||
|
||||
return (sunrise: localSunrise, sunset: localSunset)
|
||||
}
|
||||
}
|
||||
69
MeteorShowers/Models/LocationManager.swift
Normal file
69
MeteorShowers/Models/LocationManager.swift
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
protocol LocationManagerDelegate: AnyObject {
|
||||
func locationManager(_ manager: LocationManager, didUpdateLocation location: CLLocation)
|
||||
func locationManager(_ manager: LocationManager, didFailWithError error: Error)
|
||||
}
|
||||
|
||||
final class LocationManager: NSObject {
|
||||
static let shared = LocationManager()
|
||||
private let locationManager = CLLocationManager()
|
||||
weak var delegate: LocationManagerDelegate?
|
||||
var currentLocation: CLLocation?
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
locationManager.delegate = self
|
||||
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
}
|
||||
|
||||
func requestLocationUpdates() {
|
||||
print("🌍 Location: Requesting authorization...")
|
||||
locationManager.requestWhenInUseAuthorization()
|
||||
}
|
||||
}
|
||||
|
||||
extension LocationManager: CLLocationManagerDelegate {
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
print("🌍 Location: Authorization status changed to: \(manager.authorizationStatus.rawValue)")
|
||||
switch manager.authorizationStatus {
|
||||
case .authorizedWhenInUse, .authorizedAlways:
|
||||
print("🌍 Location: Authorized, starting location updates")
|
||||
locationManager.startUpdatingLocation()
|
||||
case .denied, .restricted:
|
||||
print("🌍 Location: Access denied or restricted")
|
||||
let error = NSError(domain: "LocationManager",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Location access denied"])
|
||||
delegate?.locationManager(self, didFailWithError: error)
|
||||
case .notDetermined:
|
||||
print("🌍 Location: Authorization not determined yet")
|
||||
break
|
||||
@unknown default:
|
||||
print("🌍 Location: Unknown authorization status")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
currentLocation = location
|
||||
|
||||
print("""
|
||||
🌍 Location Update Received:
|
||||
Latitude: \(location.coordinate.latitude)
|
||||
Longitude: \(location.coordinate.longitude)
|
||||
Accuracy: \(location.horizontalAccuracy)m
|
||||
Timestamp: \(location.timestamp)
|
||||
""")
|
||||
|
||||
delegate?.locationManager(self, didUpdateLocation: location)
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
print("🌍 Location Error: \(error.localizedDescription)")
|
||||
delegate?.locationManager(self, didFailWithError: error)
|
||||
}
|
||||
}
|
||||
45
MeteorShowers/Models/MeteorShower.swift
Normal file
45
MeteorShowers/Models/MeteorShower.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// MeteorShower.swift
|
||||
// MeteorShowers
|
||||
//
|
||||
// Created by Vic on 26.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MeteorShower {
|
||||
let name: String
|
||||
let dateBegin: Date
|
||||
let datePeak: Date
|
||||
let dateEnd: Date
|
||||
let zhr: Int
|
||||
let speed: Int // optional you might add this value, but it's average, I don't see any point off adding it
|
||||
let parentBody: String
|
||||
|
||||
// Formatted string getters
|
||||
var formattedBeginDate: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "dd MMM yyyy"
|
||||
return formatter.string(from: dateBegin)
|
||||
}
|
||||
|
||||
var formattedPeakDate: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "dd MMM yyyy"
|
||||
return formatter.string(from: datePeak)
|
||||
}
|
||||
|
||||
var formattedEndDate: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "dd MMM yyyy"
|
||||
return formatter.string(from: dateEnd)
|
||||
}
|
||||
|
||||
var parentBodyLabel: String {
|
||||
"\(parentBody)"
|
||||
}
|
||||
|
||||
var formattedZHR: String {
|
||||
"ZHR: \(zhr)"
|
||||
}
|
||||
}
|
||||
95
MeteorShowers/Models/RemindersManager.swift
Normal file
95
MeteorShowers/Models/RemindersManager.swift
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
final class RemindersManager {
|
||||
static let shared = RemindersManager()
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let reminderKey = "meteorShowerReminders"
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Data Structure
|
||||
struct ReminderInfo: Codable {
|
||||
let showerName: String
|
||||
let peakDate: Date
|
||||
let notificationDate: Date
|
||||
let identifier: String
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
func saveReminder(for shower: MeteorShower) {
|
||||
let identifier = "meteorShower-\(shower.name)"
|
||||
let notificationDate = Calendar.current.date(byAdding: .day, value: -3, to: shower.datePeak)!
|
||||
|
||||
let reminderInfo = ReminderInfo(
|
||||
showerName: shower.name,
|
||||
peakDate: shower.datePeak,
|
||||
notificationDate: notificationDate,
|
||||
identifier: identifier
|
||||
)
|
||||
|
||||
var reminders = getAllReminders()
|
||||
reminders.append(reminderInfo)
|
||||
saveReminders(reminders)
|
||||
}
|
||||
|
||||
func removeReminder(for shower: MeteorShower) {
|
||||
let identifier = "meteorShower-\(shower.name)"
|
||||
var reminders = getAllReminders()
|
||||
reminders.removeAll { $0.identifier == identifier }
|
||||
saveReminders(reminders)
|
||||
|
||||
// Also remove from notification center
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])
|
||||
}
|
||||
|
||||
func hasReminder(for shower: MeteorShower) -> Bool {
|
||||
let reminders = getAllReminders()
|
||||
return reminders.contains { $0.showerName == shower.name }
|
||||
}
|
||||
|
||||
func restoreAllReminders() {
|
||||
let reminders = getAllReminders()
|
||||
|
||||
for reminder in reminders {
|
||||
// Skip if notification date is in the past
|
||||
if reminder.notificationDate < Date() {
|
||||
continue
|
||||
}
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Upcoming Meteor Shower!"
|
||||
content.body = "\(reminder.showerName) meteor shower peaks in 3 days! Get ready for the show!"
|
||||
content.sound = .default
|
||||
|
||||
let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: reminder.notificationDate)
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: reminder.identifier,
|
||||
content: content,
|
||||
trigger: trigger
|
||||
)
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
print("Error restoring notification: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func getAllReminders() -> [ReminderInfo] {
|
||||
guard let data = userDefaults.data(forKey: reminderKey),
|
||||
let reminders = try? JSONDecoder().decode([ReminderInfo].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return reminders
|
||||
}
|
||||
|
||||
private func saveReminders(_ reminders: [ReminderInfo]) {
|
||||
guard let data = try? JSONEncoder().encode(reminders) else { return }
|
||||
userDefaults.set(data, forKey: reminderKey)
|
||||
}
|
||||
}
|
||||
167
MeteorShowers/Models/Showers.swift
Normal file
167
MeteorShowers/Models/Showers.swift
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import Foundation
|
||||
|
||||
final class Showers {
|
||||
// MARK: - Singleton
|
||||
static let shared = Showers()
|
||||
private init() {}
|
||||
|
||||
// MARK: - Date Formatter
|
||||
private let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "dd.MM.yyyy"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
// MARK: - Meteor Showers Data
|
||||
private lazy var showers: [MeteorShower] = {
|
||||
[
|
||||
MeteorShower(
|
||||
name: "Quadrantids",
|
||||
dateBegin: dateFormatter.date(from: "28.12.2024")!,
|
||||
datePeak: dateFormatter.date(from: "03.01.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "12.01.2025")!,
|
||||
zhr: 80,
|
||||
speed: 41,
|
||||
parentBody: "2003 EH1"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Alpha Centaurids",
|
||||
dateBegin: dateFormatter.date(from: "31.01.2025")!,
|
||||
datePeak: dateFormatter.date(from: "08.02.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "20.02.2025")!,
|
||||
zhr: 6,
|
||||
speed: 58,
|
||||
parentBody: "Unknown"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Lyrids",
|
||||
dateBegin: dateFormatter.date(from: "14.04.2025")!,
|
||||
datePeak: dateFormatter.date(from: "22.04.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "30.04.2025")!,
|
||||
zhr: 18,
|
||||
speed: 49,
|
||||
parentBody: "C/1861 G1"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Eta Aquariids",
|
||||
dateBegin: dateFormatter.date(from: "19.04.2025")!,
|
||||
datePeak: dateFormatter.date(from: "06.05.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "28.05.2025")!,
|
||||
zhr: 50,
|
||||
speed: 66,
|
||||
parentBody: "1P/Halley"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Daytime Arietids",
|
||||
dateBegin: dateFormatter.date(from: "14.05.2025")!,
|
||||
datePeak: dateFormatter.date(from: "07.06.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "24.06.2025")!,
|
||||
zhr: 30,
|
||||
speed: 38,
|
||||
parentBody: "Unknown"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Southern Delta Aquariids",
|
||||
dateBegin: dateFormatter.date(from: "12.07.2025")!,
|
||||
datePeak: dateFormatter.date(from: "31.07.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "23.08.2025")!,
|
||||
zhr: 25,
|
||||
speed: 41,
|
||||
parentBody: "96P/Machholz"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Perseids",
|
||||
dateBegin: dateFormatter.date(from: "17.07.2025")!,
|
||||
datePeak: dateFormatter.date(from: "12.08.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "24.08.2025")!,
|
||||
zhr: 100,
|
||||
speed: 59,
|
||||
parentBody: "109P/Swift-Tuttle"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Orionids",
|
||||
dateBegin: dateFormatter.date(from: "02.10.2025")!,
|
||||
datePeak: dateFormatter.date(from: "21.10.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "07.11.2025")!,
|
||||
zhr: 20,
|
||||
speed: 66,
|
||||
parentBody: "1P/Halley"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Southern Taurids",
|
||||
dateBegin: dateFormatter.date(from: "20.09.2025")!,
|
||||
datePeak: dateFormatter.date(from: "05.11.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "20.11.2025")!,
|
||||
zhr: 7,
|
||||
speed: 27,
|
||||
parentBody: "2P/Encke"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Northern Taurids",
|
||||
dateBegin: dateFormatter.date(from: "20.10.2025")!,
|
||||
datePeak: dateFormatter.date(from: "12.11.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "10.12.2025")!,
|
||||
zhr: 5,
|
||||
speed: 29,
|
||||
parentBody: "2P/Encke"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Leonids",
|
||||
dateBegin: dateFormatter.date(from: "06.11.2025")!,
|
||||
datePeak: dateFormatter.date(from: "17.11.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "30.11.2025")!,
|
||||
zhr: 10,
|
||||
speed: 71,
|
||||
parentBody: "55P/Tempel-Tuttle"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Geminids",
|
||||
dateBegin: dateFormatter.date(from: "04.12.2025")!,
|
||||
datePeak: dateFormatter.date(from: "14.12.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "20.12.2025")!,
|
||||
zhr: 150,
|
||||
speed: 35,
|
||||
parentBody: "3200 Phaethon"
|
||||
),
|
||||
MeteorShower(
|
||||
name: "Ursids",
|
||||
dateBegin: dateFormatter.date(from: "17.12.2025")!,
|
||||
datePeak: dateFormatter.date(from: "22.12.2025")!,
|
||||
dateEnd: dateFormatter.date(from: "26.12.2025")!,
|
||||
zhr: 10,
|
||||
speed: 33,
|
||||
parentBody: "8P/Tuttle"
|
||||
)
|
||||
]
|
||||
}()
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Returns all meteor showers
|
||||
func getAllShowers() -> [MeteorShower] {
|
||||
showers
|
||||
}
|
||||
|
||||
/// Returns current or upcoming meteor shower
|
||||
func getCurrentShower() -> MeteorShower? {
|
||||
let currentDate = Date()
|
||||
return showers.first { shower in
|
||||
(shower.dateBegin...shower.dateEnd).contains(currentDate)
|
||||
} ?? getNextShower()
|
||||
}
|
||||
|
||||
/// Returns the next upcoming meteor shower
|
||||
func getNextShower() -> MeteorShower? {
|
||||
let currentDate = Date()
|
||||
return showers.first { shower in
|
||||
shower.dateBegin > currentDate
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns meteor shower for specific date
|
||||
func getShower(for date: Date) -> MeteorShower? {
|
||||
showers.first { shower in
|
||||
(shower.dateBegin...shower.dateEnd).contains(date)
|
||||
}
|
||||
}
|
||||
}
|
||||
69
MeteorShowers/Models/WeatherCondition.swift
Normal file
69
MeteorShowers/Models/WeatherCondition.swift
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import Foundation
|
||||
|
||||
enum WeatherCondition: Int {
|
||||
case clearSky = 0
|
||||
case mainlyClear = 1
|
||||
case partlyCloudy = 2
|
||||
case overcast = 3
|
||||
case fog = 45
|
||||
case depositingRimeFog = 48
|
||||
case drizzleLight = 51
|
||||
case drizzleModerate = 53
|
||||
case drizzleDense = 55
|
||||
case freezingDrizzleLight = 56
|
||||
case freezingDrizzleDense = 57
|
||||
case rainSlight = 61
|
||||
case rainModerate = 63
|
||||
case rainHeavy = 65
|
||||
case freezingRainLight = 66
|
||||
case freezingRainHeavy = 67
|
||||
case snowSlight = 71
|
||||
case snowModerate = 73
|
||||
case snowHeavy = 75
|
||||
case snowGrains = 77
|
||||
case rainShowers = 80
|
||||
case rainShowersHeavy = 82
|
||||
case snowShowers = 85
|
||||
case thunderstorm = 95
|
||||
case thunderstormHailSlight = 96
|
||||
case thunderstormHailHeavy = 99
|
||||
|
||||
var systemIconName: String {
|
||||
switch self {
|
||||
case .clearSky:
|
||||
return "sun.max.fill"
|
||||
case .mainlyClear:
|
||||
return "sun.max"
|
||||
case .partlyCloudy:
|
||||
return "cloud.sun.fill"
|
||||
case .overcast:
|
||||
return "cloud.fill"
|
||||
case .fog, .depositingRimeFog:
|
||||
return "cloud.fog.fill"
|
||||
case .drizzleLight, .drizzleModerate, .drizzleDense:
|
||||
return "cloud.drizzle.fill"
|
||||
case .freezingDrizzleLight, .freezingDrizzleDense:
|
||||
return "cloud.sleet.fill"
|
||||
case .rainSlight, .rainModerate:
|
||||
return "cloud.rain.fill"
|
||||
case .rainHeavy:
|
||||
return "cloud.heavyrain.fill"
|
||||
case .freezingRainLight, .freezingRainHeavy:
|
||||
return "cloud.hail.fill"
|
||||
case .snowSlight, .snowModerate:
|
||||
return "cloud.snow.fill"
|
||||
case .snowHeavy, .snowGrains:
|
||||
return "cloud.snow.circle.fill"
|
||||
case .rainShowers:
|
||||
return "cloud.rain.fill"
|
||||
case .rainShowersHeavy:
|
||||
return "cloud.heavyrain.fill"
|
||||
case .snowShowers:
|
||||
return "cloud.snow.fill"
|
||||
case .thunderstorm:
|
||||
return "cloud.bolt.fill"
|
||||
case .thunderstormHailSlight, .thunderstormHailHeavy:
|
||||
return "cloud.bolt.rain.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
MeteorShowers/Models/WeatherData.swift
Normal file
23
MeteorShowers/Models/WeatherData.swift
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import Foundation
|
||||
|
||||
struct WeatherData: Decodable {
|
||||
let current: Current
|
||||
|
||||
struct Current: Decodable {
|
||||
let temperature: Double
|
||||
let weatherCode: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case temperature = "temperature_2m"
|
||||
case weatherCode = "weather_code"
|
||||
}
|
||||
|
||||
var condition: WeatherCondition? {
|
||||
return WeatherCondition(rawValue: weatherCode)
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case current
|
||||
}
|
||||
}
|
||||
46
MeteorShowers/Networking/NetworkManager.swift
Normal file
46
MeteorShowers/Networking/NetworkManager.swift
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// NetworkManager.swift
|
||||
// MeteorShowers
|
||||
//
|
||||
// Created by Vic on 31.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
import CoreLocation
|
||||
import OpenMeteoSdk
|
||||
|
||||
enum NetworkError: Error {
|
||||
case invalidResponse
|
||||
case decodingError
|
||||
case apiError(String)
|
||||
}
|
||||
|
||||
final class NetworkManager {
|
||||
static let shared = NetworkManager()
|
||||
private init() {}
|
||||
|
||||
private let baseURL = "https://api.open-meteo.com/v1/forecast"
|
||||
|
||||
func fetchWeather(for location: CLLocation) async throws -> WeatherData {
|
||||
let url = URL(string: "\(baseURL)?latitude=\(location.coordinate.latitude)&longitude=\(location.coordinate.longitude)¤t=temperature_2m,weather_code&timezone=auto&format=flatbuffers")!
|
||||
|
||||
do {
|
||||
let responses = try await WeatherApiResponse.fetch(url: url)
|
||||
guard let response = responses.first else {
|
||||
throw NetworkError.invalidResponse
|
||||
}
|
||||
|
||||
let current = response.current!
|
||||
|
||||
return WeatherData(
|
||||
current: .init(
|
||||
temperature: Double(current.variables(at: 0)!.value),
|
||||
weatherCode: Int(current.variables(at: 1)!.value)
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
throw NetworkError.apiError(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
111
MeteorShowers/ViewControllers/MainViewController.swift
Normal file
111
MeteorShowers/ViewControllers/MainViewController.swift
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//
|
||||
// ViewController.swift
|
||||
// MeteorShowers
|
||||
//
|
||||
// Created by Vic on 26.01.2025.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class MainViewController: UIViewController {
|
||||
|
||||
// MARK: - Properties
|
||||
private let showers = Showers.shared.getAllShowers()
|
||||
|
||||
// MARK: - Main stack
|
||||
private lazy var mainStackView: UIStackView = {
|
||||
var stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 8
|
||||
stackView.alignment = .fill
|
||||
stackView.distribution = .fill
|
||||
return stackView
|
||||
}()
|
||||
|
||||
// MARK: - creating view with uiview extension
|
||||
private lazy var topView: TopView = .fromNib()
|
||||
|
||||
// MARK: - UITableView
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView()
|
||||
table.backgroundColor = .clear
|
||||
table.separatorStyle = .none
|
||||
table.showsVerticalScrollIndicator = false
|
||||
return table
|
||||
}()
|
||||
|
||||
// MARK: - viewDidLoad
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .darkMidnight
|
||||
setupUI()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configuring UI
|
||||
private extension MainViewController {
|
||||
|
||||
func setupUI() {
|
||||
setDelegates()
|
||||
|
||||
view.addSubview(mainStackView)
|
||||
mainStackView.addArrangedSubview(topView)
|
||||
mainStackView.addArrangedSubview(tableView)
|
||||
|
||||
// MARK: - Cell registration
|
||||
tableView.register(MeteorShowerTableViewCell.self, forCellReuseIdentifier: "MeteorShowerTableViewCell")
|
||||
|
||||
// constraints
|
||||
mainStackView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
make.bottom.equalToSuperview()
|
||||
make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
|
||||
}
|
||||
|
||||
topView.snp.makeConstraints { make in
|
||||
make.height.equalTo(100)
|
||||
}
|
||||
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegates
|
||||
extension MainViewController: UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
func setDelegates() {
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return showers.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "MeteorShowerTableViewCell", for: indexPath) as! MeteorShowerTableViewCell
|
||||
let shower = showers[indexPath.row]
|
||||
cell.configure(with: shower)
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 115
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let detailViewController = ShowerDetailViewController()
|
||||
let selectedShower = showers[indexPath.row]
|
||||
detailViewController.configure(with: selectedShower)
|
||||
|
||||
if let navigationController = navigationController {
|
||||
navigationController.pushViewController(detailViewController, animated: true)
|
||||
}
|
||||
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
343
MeteorShowers/ViewControllers/ShowerDetailViewController.swift
Normal file
343
MeteorShowers/ViewControllers/ShowerDetailViewController.swift
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
//
|
||||
// ShowerDetailViewController.swift
|
||||
// MeteorShowers
|
||||
//
|
||||
// Created by Vic on 29.01.2025.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
import UserNotifications
|
||||
|
||||
final class ShowerDetailViewController: UIViewController {
|
||||
|
||||
// MARK: - Properties
|
||||
private let moonPhaseCalculator = MoonPhaseCalculation()
|
||||
private let remindersManager = RemindersManager.shared
|
||||
private var currentShower: MeteorShower?
|
||||
private var hasReminder = false
|
||||
private lazy var reminderButton: UIBarButtonItem = {
|
||||
let button = UIBarButtonItem(
|
||||
image: UIImage(systemName: "bell.fill"),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(reminderButtonTapped)
|
||||
)
|
||||
button.tintColor = .systemGreen
|
||||
return button
|
||||
}()
|
||||
|
||||
// MARK: - Main stack
|
||||
lazy private var mainStackView: UIStackView = {
|
||||
var stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 8
|
||||
stackView.alignment = .fill
|
||||
stackView.distribution = .fillEqually
|
||||
|
||||
return stackView
|
||||
}()
|
||||
|
||||
// MARK: - content stack
|
||||
lazy private var imageStackView: UIStackView = {
|
||||
var stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 8
|
||||
stackView.alignment = .center
|
||||
stackView.distribution = .equalCentering
|
||||
|
||||
return stackView
|
||||
}()
|
||||
|
||||
lazy private var infoStackView: UIStackView = {
|
||||
var stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 8
|
||||
stackView.alignment = .fill
|
||||
stackView.distribution = .fillEqually
|
||||
|
||||
return stackView
|
||||
}()
|
||||
|
||||
lazy private var bodyPeakAndZHRStackView: UIStackView = {
|
||||
var stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 8
|
||||
stackView.alignment = .leading
|
||||
stackView.distribution = .fillEqually
|
||||
|
||||
return stackView
|
||||
}()
|
||||
|
||||
lazy private var moonStackView: UIStackView = {
|
||||
var stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 8
|
||||
stackView.alignment = .fill
|
||||
stackView.distribution = .fillEqually
|
||||
|
||||
return stackView
|
||||
}()
|
||||
|
||||
lazy private var descriptionStackView: UIStackView = {
|
||||
var stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .fill
|
||||
stackView.distribution = .fillEqually
|
||||
|
||||
return stackView
|
||||
}()
|
||||
|
||||
lazy private var mainImageView: UIImageView = {
|
||||
var imageView = UIImageView()
|
||||
imageView.image = UIImage(named: "mock.jpeg")
|
||||
imageView.contentMode = .scaleToFill
|
||||
imageView.clipsToBounds = true
|
||||
imageView.backgroundColor = .lightMidnight
|
||||
imageView.layer.cornerRadius = 12
|
||||
return imageView
|
||||
}()
|
||||
|
||||
lazy private var notificationIcon: UIImageView = {
|
||||
var imageView = UIImageView()
|
||||
imageView.image = UIImage(systemName: "bell.fill")
|
||||
imageView.tintColor = .systemGreen
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.snp.makeConstraints { make in
|
||||
make.height.width.equalTo(24)
|
||||
}
|
||||
return imageView
|
||||
}()
|
||||
|
||||
lazy private var peakLabel: UILabel = {
|
||||
var label = UILabel()
|
||||
label.font = .systemFont(ofSize: 17, weight: .bold)
|
||||
label.textColor = .lightGray
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy private var ZHRLabel: UILabel = {
|
||||
var label = UILabel()
|
||||
label.font = .systemFont(ofSize: 18, weight: .bold)
|
||||
label.textColor = .lightGray
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy private var originLabel: UILabel = {
|
||||
var label = UILabel()
|
||||
label.font = .systemFont(ofSize: 14, weight: .thin)
|
||||
label.textColor = .lightGray
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy private var moonPhaseLabel: UILabel = {
|
||||
var label = UILabel()
|
||||
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
label.textColor = .lightGray
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy private var moonPhaseIcon: UIImageView = {
|
||||
var imageView = UIImageView()
|
||||
imageView.image = UIImage(systemName: "moon")
|
||||
imageView.tintColor = .lightGray
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
return imageView
|
||||
}()
|
||||
|
||||
lazy private var descriptionLabel: UILabel = {
|
||||
var label = UILabel()
|
||||
label.font = .systemFont(ofSize: 14, weight: .bold)
|
||||
label.textAlignment = .center
|
||||
label.text = "description"
|
||||
return label
|
||||
}()
|
||||
|
||||
|
||||
// MARK: - life cycle method
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .darkMidnight
|
||||
setupUI()
|
||||
setupNavigationBar()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Setup UI
|
||||
extension ShowerDetailViewController {
|
||||
func setupUI() {
|
||||
view.addSubview(mainStackView)
|
||||
|
||||
mainStackView.addArrangedSubview(imageStackView)
|
||||
mainStackView.addArrangedSubview(infoStackView)
|
||||
mainStackView.addArrangedSubview(descriptionStackView)
|
||||
|
||||
imageStackView.addArrangedSubview(mainImageView)
|
||||
|
||||
infoStackView.addArrangedSubview(bodyPeakAndZHRStackView)
|
||||
infoStackView.addArrangedSubview(moonStackView)
|
||||
|
||||
bodyPeakAndZHRStackView.addArrangedSubview(peakLabel)
|
||||
bodyPeakAndZHRStackView.addArrangedSubview(ZHRLabel)
|
||||
bodyPeakAndZHRStackView.addArrangedSubview(originLabel)
|
||||
|
||||
moonStackView.addArrangedSubview(moonPhaseIcon)
|
||||
moonStackView.addArrangedSubview(moonPhaseLabel)
|
||||
|
||||
mainStackView.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(16)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
|
||||
mainImageView.snp.makeConstraints { make in
|
||||
make.height.equalTo(210)
|
||||
make.width.equalTo(imageStackView.snp.width)
|
||||
}
|
||||
|
||||
imageStackView.snp.makeConstraints { make in
|
||||
make.height.equalTo(210)
|
||||
}
|
||||
|
||||
moonPhaseIcon.snp.makeConstraints { make in
|
||||
make.height.width.equalTo(48)
|
||||
make.centerX.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNavigationBar() {
|
||||
navigationItem.rightBarButtonItem = reminderButton
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
updateReminderButtonState()
|
||||
}
|
||||
|
||||
private func updateReminderButtonState() {
|
||||
guard let shower = currentShower else { return }
|
||||
|
||||
let hasReminder = remindersManager.hasReminder(for: shower)
|
||||
self.hasReminder = hasReminder
|
||||
reminderButton.image = UIImage(systemName: hasReminder ? "bell.slash.fill" : "bell.fill")
|
||||
}
|
||||
|
||||
@objc private func reminderButtonTapped() {
|
||||
if hasReminder {
|
||||
removeNotification()
|
||||
} else {
|
||||
requestNotificationPermission()
|
||||
}
|
||||
}
|
||||
|
||||
private func removeNotification() {
|
||||
guard let shower = currentShower else { return }
|
||||
|
||||
remindersManager.removeReminder(for: shower)
|
||||
|
||||
// Update UI and show feedback
|
||||
hasReminder = false
|
||||
reminderButton.image = UIImage(systemName: "bell.fill")
|
||||
|
||||
let alert = UIAlertController(
|
||||
title: "Reminder Removed",
|
||||
message: "The reminder for \(shower.name) meteor shower has been removed.",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func requestNotificationPermission() {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in
|
||||
if granted {
|
||||
DispatchQueue.main.async {
|
||||
self?.scheduleNotification()
|
||||
}
|
||||
} else {
|
||||
// Handle case when permission is not granted
|
||||
print("Notification permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleNotification() {
|
||||
guard let shower = currentShower else { return }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Upcoming Meteor Shower!"
|
||||
content.body = "\(shower.name) meteor shower peaks in 3 days! Get ready for the show!"
|
||||
content.sound = .default
|
||||
|
||||
// Calculate notification date (3 days before peak)
|
||||
let peakDate = shower.datePeak
|
||||
let notificationDate = Calendar.current.date(byAdding: .day, value: -3, to: peakDate)!
|
||||
let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: notificationDate)
|
||||
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
|
||||
let request = UNNotificationRequest(
|
||||
identifier: "meteorShower-\(shower.name)",
|
||||
content: content,
|
||||
trigger: trigger
|
||||
)
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { [weak self] error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
print("Error scheduling notification: \(error)")
|
||||
self?.showNotificationAlert(success: false)
|
||||
} else {
|
||||
print("Notification scheduled for \(shower.name)")
|
||||
// Save to persistent storage
|
||||
self?.remindersManager.saveReminder(for: shower)
|
||||
self?.hasReminder = true
|
||||
self?.reminderButton.image = UIImage(systemName: "bell.slash.fill")
|
||||
self?.showNotificationAlert(success: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showNotificationAlert(success: Bool) {
|
||||
let title = success ? "Reminder Set!" : "Error"
|
||||
let message = success ?
|
||||
"You'll be notified 3 days before the \(currentShower?.name ?? "") meteor shower peaks." :
|
||||
"Failed to set reminder. Please check if notifications are enabled in Settings."
|
||||
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fill with contents
|
||||
|
||||
extension ShowerDetailViewController {
|
||||
func configure(with shower: MeteorShower) {
|
||||
currentShower = shower
|
||||
navigationItem.title = shower.name
|
||||
peakLabel.text = "Peak: \(shower.formattedPeakDate)"
|
||||
ZHRLabel.text = "\(shower.formattedZHR)"
|
||||
originLabel.text = "Parent body: \(shower.parentBodyLabel)"
|
||||
|
||||
// Calculate moon phase for shower's peak date
|
||||
let moonPhase = moonPhaseCalculator.getMoonPhase(date: shower.datePeak)
|
||||
if let phase = MoonPhase(rawValue: moonPhase.phase) {
|
||||
moonPhaseIcon.image = phase.icon
|
||||
moonPhaseLabel.text = moonPhase.phase
|
||||
}
|
||||
|
||||
// Check if reminder exists
|
||||
updateReminderButtonState()
|
||||
}
|
||||
|
||||
private func updateMoonPhase() {
|
||||
let moonInfo = moonPhaseCalculator.getMoonPhase()
|
||||
updateMoonPhaseUI(with: moonInfo)
|
||||
}
|
||||
|
||||
private func updateMoonPhaseUI(with moonInfo: (phase: String, illumination: Double, age: Double)) {
|
||||
moonPhaseLabel.text = moonInfo.phase
|
||||
}
|
||||
}
|
||||
BIN
MeteorShowers/Views/.DS_Store
vendored
Normal file
BIN
MeteorShowers/Views/.DS_Store
vendored
Normal file
Binary file not shown.
141
MeteorShowers/Views/TopView.swift
Normal file
141
MeteorShowers/Views/TopView.swift
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
//
|
||||
// TopView.swift
|
||||
// MeteorShowers
|
||||
//
|
||||
// Created by Vic on 26.01.2025.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreLocation
|
||||
|
||||
final class TopView: UIView {
|
||||
|
||||
// MARK: - Properties
|
||||
private let moonPhaseCalculator = MoonPhaseCalculation()
|
||||
private let sunCalculator = SunRiseSetCalc()
|
||||
private let locationManager = LocationManager.shared
|
||||
private lazy var dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
formatter.timeZone = TimeZone(secondsFromGMT: 0) // Use UTC to avoid double timezone conversion
|
||||
return formatter
|
||||
}()
|
||||
|
||||
// MARK: - UIs
|
||||
@IBOutlet weak var sunriseTimeLabel: UILabel!
|
||||
@IBOutlet weak var sunsetTimeLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var tempLabel: UILabel!
|
||||
@IBOutlet weak var forecastIconImageView: UIImageView!
|
||||
|
||||
@IBOutlet weak var moonPhaseIconImageView: UIImageView!
|
||||
@IBOutlet weak var IlluminationLabel: UILabel!
|
||||
@IBOutlet weak var moonPhaseLabel: UILabel!
|
||||
|
||||
// MARK: - Lifecycle
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
setupUI()
|
||||
setupLocation()
|
||||
updateMoonPhase()
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func setupUI() {
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 9
|
||||
self.layer.masksToBounds = true
|
||||
|
||||
// Set initial values
|
||||
sunriseTimeLabel.text = "--:--"
|
||||
sunsetTimeLabel.text = "--:--"
|
||||
}
|
||||
|
||||
private func setupLocation() {
|
||||
locationManager.delegate = self
|
||||
locationManager.requestLocationUpdates()
|
||||
}
|
||||
|
||||
private func updateMoonPhase() {
|
||||
let moonInfo = moonPhaseCalculator.getMoonPhase()
|
||||
updateMoonPhaseUI(with: moonInfo)
|
||||
}
|
||||
|
||||
private func updateMoonPhaseUI(with moonInfo: (phase: String, illumination: Double, age: Double)) {
|
||||
moonPhaseLabel.text = moonInfo.phase
|
||||
IlluminationLabel.text = String(format: "%.1f%%", moonInfo.illumination)
|
||||
|
||||
if let moonPhase = MoonPhase.allCases.first(where: { $0.rawValue == moonInfo.phase }) {
|
||||
moonPhaseIconImageView.image = moonPhase.icon
|
||||
moonPhaseIconImageView.tintColor = .lightGray
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSunTimes(for location: CLLocation) {
|
||||
print("🌅 Calculating sun times for location: \(location.coordinate.latitude), \(location.coordinate.longitude)")
|
||||
|
||||
// Get today's date at midnight in current timezone
|
||||
let calendar = Calendar.current
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
|
||||
let times = sunCalculator.calculateSunriseSunset(for: today, at: location)
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let sunrise = times.sunrise {
|
||||
let timeString = self.dateFormatter.string(from: sunrise)
|
||||
print("🌅 Sunrise calculated: \(timeString) (Date: \(sunrise))")
|
||||
self.sunriseTimeLabel.text = timeString
|
||||
} else {
|
||||
print("🌅 No sunrise calculated")
|
||||
self.sunriseTimeLabel.text = "No sunrise"
|
||||
}
|
||||
|
||||
if let sunset = times.sunset {
|
||||
let timeString = self.dateFormatter.string(from: sunset)
|
||||
print("🌅 Sunset calculated: \(timeString) (Date: \(sunset))")
|
||||
self.sunsetTimeLabel.text = timeString
|
||||
} else {
|
||||
print("🌅 No sunset calculated")
|
||||
self.sunsetTimeLabel.text = "No sunset"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateWeather(for location: CLLocation) {
|
||||
Task {
|
||||
do {
|
||||
let weatherData = try await NetworkManager.shared.fetchWeather(for: location)
|
||||
await MainActor.run {
|
||||
tempLabel.text = String(format: "%.1f°C", weatherData.current.temperature)
|
||||
if let condition = weatherData.current.condition {
|
||||
forecastIconImageView.image = UIImage(systemName: condition.systemIconName)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error fetching weather: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LocationManagerDelegate
|
||||
extension TopView: LocationManagerDelegate {
|
||||
func locationManager(_ manager: LocationManager, didUpdateLocation location: CLLocation) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.updateSunTimes(for: location)
|
||||
self?.updateWeather(for: location)
|
||||
}
|
||||
}
|
||||
|
||||
func locationManager(_ manager: LocationManager, didFailWithError error: Error) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
// Use Moscow coordinates as fallback
|
||||
let moscowLocation = CLLocation(latitude: 55.7558, longitude: 37.6173)
|
||||
self?.updateSunTimes(for: moscowLocation)
|
||||
self?.updateWeather(for: moscowLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
167
MeteorShowers/Views/TopView.xib
Normal file
167
MeteorShowers/Views/TopView.xib
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Image references" minToolsVersion="12.0"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" id="iN0-l3-epB" customClass="TopView" customModule="MeteorShowers" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="363" height="150"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" spacing="3" translatesAutoresizingMaskIntoConstraints="NO" id="Lh3-TW-hc0" userLabel="Main Stack View">
|
||||
<rect key="frame" x="8" y="8" width="347" height="134"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="7fE-rz-sBk" userLabel="Sun Stack View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="115" height="134"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="g89-qq-Cmg" userLabel="Rise Stack View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="115" height="67"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JB1-to-8LG" userLabel="Icon Stack View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="57.333333333333336" height="67"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="DK8-fw-qyw">
|
||||
<rect key="frame" x="0.0" y="2.3333333333333357" width="57.333333333333336" height="63.666666666666664"/>
|
||||
<imageReference key="image" image="sunrise.fill" catalog="system" renderingMode="original"/>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="small"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="tIs-S8-mpT" userLabel="Time Stack View">
|
||||
<rect key="frame" x="57.333333333333329" y="0.0" width="57.666666666666671" height="67"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="07:02" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dbT-fC-rK4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="57.666666666666664" height="67"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="bo6-lk-qjK" userLabel="Dusk Stack View">
|
||||
<rect key="frame" x="0.0" y="67" width="115" height="67"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6Ri-pq-LPg" userLabel="Icon Stack View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="57.333333333333336" height="67"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="KxC-DV-xtd">
|
||||
<rect key="frame" x="0.0" y="2.3333333333333357" width="57.333333333333336" height="63.666666666666664"/>
|
||||
<imageReference key="image" image="sunset.fill" catalog="system" renderingMode="original"/>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="small"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="U5G-D2-6Nx" userLabel="Time Stack View">
|
||||
<rect key="frame" x="57.333333333333329" y="0.0" width="57.666666666666671" height="67"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="18:13" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mLN-r9-qnP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="57.666666666666664" height="67"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="Rbp-Ks-0Jy" userLabel="Forecast Stack View">
|
||||
<rect key="frame" x="118" y="0.0" width="52" height="134"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9UP-Yn-lHd" userLabel="Temp Stack View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="52" height="67"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="+ 3 C" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LsH-ur-UYD">
|
||||
<rect key="frame" x="0.0" y="0.0" width="52" height="67"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="HBb-Qs-RY1" userLabel="Icon Stack View">
|
||||
<rect key="frame" x="0.0" y="67" width="52" height="67"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="ZZk-Nb-Urp">
|
||||
<rect key="frame" x="0.0" y="0.3333333333333357" width="52.666666666666671" height="66.666666666666657"/>
|
||||
<imageReference key="image" image="cloud.sun.fill" catalog="system" renderingMode="original"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="kxa-nk-kcN" userLabel="Moon Stack View">
|
||||
<rect key="frame" x="173" y="0.0" width="174" height="134"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="ljf-yW-EmY" userLabel="Info Stack View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="174" height="67"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Aa7-Pz-BTs">
|
||||
<rect key="frame" x="0.0" y="0.6666666666666643" width="87" height="65.666666666666686"/>
|
||||
<imageReference key="image" image="moonphase.waning.crescent.inverse" catalog="system" renderingMode="original"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="11%" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FvE-Tv-ohL">
|
||||
<rect key="frame" x="87" y="0.0" width="87" height="67"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="XRy-3E-eO3" userLabel="Phase Stack View">
|
||||
<rect key="frame" x="0.0" y="67" width="174" height="67"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Waning Crescending" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tci-qO-cjF">
|
||||
<rect key="frame" x="0.0" y="0.0" width="174" height="67"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" name="light_midnight"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="Lh3-TW-hc0" secondAttribute="bottom" constant="8" id="1lL-vv-QbN"/>
|
||||
<constraint firstItem="Lh3-TW-hc0" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="8" id="Wx3-1i-78V"/>
|
||||
<constraint firstItem="Lh3-TW-hc0" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="8" id="o5I-3e-bEd"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="Lh3-TW-hc0" secondAttribute="trailing" constant="8" id="s3B-YN-M0u"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="IlluminationLabel" destination="FvE-Tv-ohL" id="yy2-E3-Bkw"/>
|
||||
<outlet property="forecastIconImageView" destination="ZZk-Nb-Urp" id="PCh-Tj-ifl"/>
|
||||
<outlet property="moonPhaseIconImageView" destination="Aa7-Pz-BTs" id="98K-4b-CJe"/>
|
||||
<outlet property="moonPhaseLabel" destination="tci-qO-cjF" id="I07-lb-YyX"/>
|
||||
<outlet property="sunriseTimeLabel" destination="dbT-fC-rK4" id="7rz-g9-6vt"/>
|
||||
<outlet property="sunsetTimeLabel" destination="mLN-r9-qnP" id="MSN-1f-KaJ"/>
|
||||
<outlet property="tempLabel" destination="LsH-ur-UYD" id="Eft-4N-OCI"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="107.63358778625954" y="241.5492957746479"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="cloud.sun.fill" catalog="system" width="128" height="101"/>
|
||||
<image name="moonphase.waning.crescent.inverse" catalog="system" width="128" height="123"/>
|
||||
<image name="sunrise.fill" catalog="system" width="128" height="104"/>
|
||||
<image name="sunset.fill" catalog="system" width="128" height="104"/>
|
||||
<namedColor name="light_midnight">
|
||||
<color red="0.11673399925956369" green="0.19502729158678409" blue="0.36641927908376315" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
BIN
showers.pdf
Normal file
BIN
showers.pdf
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue