diff --git a/README.md b/README.md index e0926fc..00e0a78 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## What It Is: * An iOS Weather App that uses Apple's WeatherKit API - +
Image 1 Image 2 @@ -14,6 +14,8 @@ Image 5
+--- + ### [App Store Link](https://apps.apple.com/us/app/stormylaunch-weather-app/id6444372213) ## Background Info: @@ -28,8 +30,9 @@ Minimum iOS: 16.0 Dependencies: None ``` -The iOS app is built mostly in UIKit, with some occasionsal SwiftUI elements -The WatchOS app is built entirelty in SwiftUI +The iOS app is built mostly in UIKit, with some occasionsal SwiftUI elements. +Also has support for iPad. +The WatchOS app is built entirelty in SwiftUI. ## Contributing diff --git a/programmatic-ui-weather-app/Delegates/SceneDelegate.swift b/programmatic-ui-weather-app/Delegates/SceneDelegate.swift index 4bdb001..946563f 100644 --- a/programmatic-ui-weather-app/Delegates/SceneDelegate.swift +++ b/programmatic-ui-weather-app/Delegates/SceneDelegate.swift @@ -19,10 +19,27 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(frame: UIScreen.main.bounds) - let home = TabBarViewController() - self.window?.rootViewController = TabBarViewController() - window?.windowScene = windowScene - window?.makeKeyAndVisible() + + switch UIDevice.current.userInterfaceIdiom { + case .phone: + // It's an iPhone + print("iphone") + let home = TabBarViewController() + self.window?.rootViewController = TabBarViewController() + window?.windowScene = windowScene + window?.makeKeyAndVisible() + case .pad: + // It's an iPad (or macOS Catalyst) + print("ipad") + let home = iPadMainViewController() + self.window?.rootViewController = iPadMainViewController() + window?.windowScene = windowScene + window?.makeKeyAndVisible() + + @unknown default: + // Uh, oh! What could it be? + print("oops, unknown device") + } } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/programmatic-ui-weather-app/Info.plist b/programmatic-ui-weather-app/Info.plist index 1a0945a..39258ce 100644 --- a/programmatic-ui-weather-app/Info.plist +++ b/programmatic-ui-weather-app/Info.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + com.ES.refresh + UIAppFonts SpaceX.ttf @@ -31,9 +35,5 @@ fetch - BGTaskSchedulerPermittedIdentifiers - - com.ES.refresh - diff --git a/programmatic-ui-weather-app/Utils/WeatherKitCallUtil.swift b/programmatic-ui-weather-app/Utils/WeatherKitCallUtil.swift index 5208dd2..66ebe05 100644 --- a/programmatic-ui-weather-app/Utils/WeatherKitCallUtil.swift +++ b/programmatic-ui-weather-app/Utils/WeatherKitCallUtil.swift @@ -53,6 +53,7 @@ extension MainViewController { MF0.numberFormatter.maximumFractionDigits = 3 let uv = result.0.uvIndex.value + let uvCategory = result.0.uvIndex.category.description let windSpeed = MF0.string(from: result.0.wind.speed) print(windSpeed) let windDirection = result.0.wind.compassDirection @@ -143,6 +144,7 @@ extension MainViewController { WeatherKitData.TempMax = tempMax WeatherKitData.TempMin = tempMin WeatherKitData.UV = uv + WeatherKitData.UVCategory = uvCategory WeatherKitData.WindSpeed = windSpeed WeatherKitData.WindDirection = "\(windDirection)" WeatherKitData.Symbol = symbol @@ -177,6 +179,7 @@ extension MainViewController { } } //@MainActor runs after getweather Task completes + //@MainActor ensures all code runs on main dispatch thread @MainActor private func updateLabelsAfterAwait() { ForecastListVC().forecastTableView.reloadData() @@ -187,6 +190,7 @@ extension MainViewController { let date = WeatherKitData.SunsetDate let interval = WeatherKitData.SunsetDate.timeIntervalSinceReferenceDate - WeatherKitData.SunriseDate.timeIntervalSinceReferenceDate + print("interval time: \(interval)") let rocketTimer = Timer(fireAt: date, interval: interval, target: self, selector: #selector(AnimateRocket), userInfo: nil, repeats: false) RunLoop.main.add(rocketTimer, forMode: RunLoop.Mode.common) diff --git a/programmatic-ui-weather-app/View Controllers/MainVC/HourlyCollectionViewCell/HourlyCollectionViewCell.swift b/programmatic-ui-weather-app/View Controllers/MainVC/HourlyCollectionViewCell/HourlyCollectionViewCell.swift index 55c8e50..8e23c96 100644 --- a/programmatic-ui-weather-app/View Controllers/MainVC/HourlyCollectionViewCell/HourlyCollectionViewCell.swift +++ b/programmatic-ui-weather-app/View Controllers/MainVC/HourlyCollectionViewCell/HourlyCollectionViewCell.swift @@ -29,6 +29,8 @@ class CustomCell: UICollectionViewCell { private func setupUI() { // bigger top stackview topStack = UIStackView() +// topStack.applyBlurEffect(cornerRadius: 15) +// topStack.layer.cornerRadius = 15 // individual stackview stackView = UIStackView() diff --git a/programmatic-ui-weather-app/View Controllers/MainVC/MainViewController.swift b/programmatic-ui-weather-app/View Controllers/MainVC/iPhoneMainViewController.swift similarity index 86% rename from programmatic-ui-weather-app/View Controllers/MainVC/MainViewController.swift rename to programmatic-ui-weather-app/View Controllers/MainVC/iPhoneMainViewController.swift index dbafa71..ed34e2b 100644 --- a/programmatic-ui-weather-app/View Controllers/MainVC/MainViewController.swift +++ b/programmatic-ui-weather-app/View Controllers/MainVC/iPhoneMainViewController.swift @@ -40,6 +40,34 @@ class MainViewController: UIViewController, CLLocationManagerDelegate, UIScrollV let windTitleLabel = UILabel() let windLabel = UILabel() + // uvView variables + let uvView = UIView() + let uvTitleLabel = UILabel() + private let uvProgressView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.cornerRadius = 4 + view.clipsToBounds = true + view.backgroundColor = UIColor.systemGray2.withAlphaComponent(0.3) // Add this line + return view + }() + + private let uvGradientLayer: CAGradientLayer = { + let layer = CAGradientLayer() + layer.colors = [ + UIColor.systemGreen.cgColor, + UIColor.systemYellow.cgColor, + UIColor.systemOrange.cgColor, + UIColor.systemRed.cgColor, + UIColor.systemPurple.cgColor + ] + layer.startPoint = CGPoint(x: 0, y: 0.5) + layer.endPoint = CGPoint(x: 1, y: 0.5) + layer.cornerRadius = 2 // Add this line to match the view's corner radius + return layer + }() + + // collection view for hourly forecast let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout.init()) // need to have frame and layout for UICollectionView @@ -50,7 +78,6 @@ class MainViewController: UIViewController, CLLocationManagerDelegate, UIScrollV //Creates view for cloud at top let uiView = UIView() - override func viewDidLoad() { super.viewDidLoad() @@ -209,7 +236,7 @@ class MainViewController: UIViewController, CLLocationManagerDelegate, UIScrollV windView.layer.cornerRadius = 20 windView.isUserInteractionEnabled = true windView.addGestureRecognizer(windTapGesture) -// windView.applyBlurEffect(.systemUltraThinMaterialLight, cornerRadius: 20) +// windView.applyBlurEffect(.systemUltraThinMaterialDark, cornerRadius: 20) windTitleLabel.translatesAutoresizingMaskIntoConstraints = false windTitleLabel.text = "Wind" @@ -219,6 +246,15 @@ class MainViewController: UIViewController, CLLocationManagerDelegate, UIScrollV windLabel.text = "--MPH" windLabel.font = .preferredFont(forTextStyle: .title1) + uvView.translatesAutoresizingMaskIntoConstraints = false + uvView.backgroundColor = cyanColor + uvView.layer.cornerRadius = 20 +// uvView.applyBlurEffect(.systemUltraThinMaterialDark, cornerRadius: 20) + + uvTitleLabel.translatesAutoresizingMaskIntoConstraints = false + uvTitleLabel.text = "UV" + uvTitleLabel.font = .preferredFont(forTextStyle: .body) + //Sets settings for refreshControl refreshControl.translatesAutoresizingMaskIntoConstraints = false refreshControl.attributedTitle = NSAttributedString("Fetching Weather") @@ -236,6 +272,7 @@ class MainViewController: UIViewController, CLLocationManagerDelegate, UIScrollV scrollView.addSubview(precipitationView) scrollView.addSubview(hourlyForecastView) scrollView.addSubview(windView) + scrollView.addSubview(uvView) scrollView.addSubview(refreshControl) //Adds elements into precipitationView @@ -246,6 +283,12 @@ class MainViewController: UIViewController, CLLocationManagerDelegate, UIScrollV windView.addSubview(windTitleLabel) windView.addSubview(windLabel) + // Adds elements into uvView + uvView.addSubview(uvTitleLabel) + uvView.addSubview(uvProgressView) + + uvProgressView.layer.addSublayer(uvGradientLayer) + //Adds elements into hourlyForecastView hourlyForecastView.addSubview(hourlyForecastTitleLabel) hourlyForecastView.addSubview(collectionView) @@ -316,6 +359,19 @@ class MainViewController: UIViewController, CLLocationManagerDelegate, UIScrollV //windLabel constraints windLabel.centerYAnchor.constraint(equalTo: windView.centerYAnchor), windLabel.centerXAnchor.constraint(equalTo: windView.centerXAnchor), + // uvView contraints + uvView.topAnchor.constraint(equalTo: windView.bottomAnchor, constant: 20), + uvView.leadingAnchor.constraint(equalTo: windView.leadingAnchor), + uvView.trailingAnchor.constraint(equalTo: windView.trailingAnchor), + uvView.heightAnchor.constraint(equalToConstant: 100), + // uvTitleLabel constraints + uvTitleLabel.topAnchor.constraint(equalTo: uvView.topAnchor, constant: 10), + uvTitleLabel.leadingAnchor.constraint(equalTo: uvView.leadingAnchor, constant: 10), + // uvProgressView constraints + uvProgressView.topAnchor.constraint(equalTo: uvTitleLabel.bottomAnchor, constant: 10), + uvProgressView.heightAnchor.constraint(equalToConstant: 6), + uvProgressView.leadingAnchor.constraint(equalTo: uvView.leadingAnchor, constant: 10), + uvProgressView.trailingAnchor.constraint(equalTo: uvView.trailingAnchor, constant: -10) ])//Constraint Array }//Layout func }//MainViewController class @@ -379,6 +435,7 @@ extension MainViewController { self.todayTempLabel.text = "H:\(WeatherKitData.TempMax) L:\(WeatherKitData.TempMin)" self.windLabel.text = "\(WeatherKitData.WindSpeed)" self.precipitationLabel.text = "\(WeatherKitData.PrecipitationChance)% Chance" + self.updateUVIndex(WeatherKitData.UV) DateConverter().timeArrayMaker() } @@ -407,6 +464,34 @@ extension MainViewController { fetchFromReload() } + // Stuff so that uvGradientLayer frame works + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + // Update gradient layer frame + uvGradientLayer.frame = uvProgressView.bounds + } + + // Function to update the UVIndex + func updateUVIndex(_ value: Int) { + print("skibb: \(WeatherKitData.UVCategory)") + // Create a mask layer to achieve the progress bar effect + let maskLayer = CALayer() + let clampedValue = min(max(value, 0), 11) + let percentage = CGFloat(clampedValue) / 11.0 + + // The gradient layer should always be full width + uvGradientLayer.frame = uvProgressView.bounds + + // Create a mask that only shows a portion of the gradient + maskLayer.frame = CGRect(x: 0, y: 0, + width: uvProgressView.bounds.width * percentage, + height: uvProgressView.bounds.height) + maskLayer.backgroundColor = UIColor.white.cgColor + + // Apply the mask to the gradient layer + uvGradientLayer.mask = maskLayer + } + //Creates a function for running fetchWeather from reload func func fetchFromReload() { DispatchQueue.main.async { @@ -548,10 +633,12 @@ extension MainViewController { } } + // number of hourly cells func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 6 } + // cell creator func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell @@ -565,6 +652,7 @@ extension MainViewController { } } else { cell.weatherIcon.image = UIImage(systemName: "questionmark")} + // displays them only if more than 6 items in HourluForecast if WeatherKitData.HourlyForecast.count > 6 { cell.tempLabel.text = "\(Int((round(WeatherKitData.HourlyForecast[indexPath.row])*100)/100))˚" } else { cell.tempLabel.text = "--" } @@ -575,4 +663,5 @@ extension MainViewController { return cell } + } diff --git a/programmatic-ui-weather-app/WeatherStruct/WeatherKitData.swift b/programmatic-ui-weather-app/WeatherStruct/WeatherKitData.swift index 3dcf911..da262dd 100644 --- a/programmatic-ui-weather-app/WeatherStruct/WeatherKitData.swift +++ b/programmatic-ui-weather-app/WeatherStruct/WeatherKitData.swift @@ -39,8 +39,10 @@ struct WeatherKitData: Codable{ static var TempFeels = 0 static var UV = 0 + static var UVCategory = "" static var WindDirection = "" + static var WindDirectionAngle = 0.0 static var WindGusts = [Measurement]() static var WindSpeed = "" static var WindSpeedForecast = [0.0] diff --git a/programmatic-ui-weather-app/iPad Version/Rocket Utils/LaunchRocket.swift b/programmatic-ui-weather-app/iPad Version/Rocket Utils/LaunchRocket.swift new file mode 100644 index 0000000..ba72de4 --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/Rocket Utils/LaunchRocket.swift @@ -0,0 +1,26 @@ +// +// LaunchRocket.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 11/4/23. +// +import UIKit +import Foundation + + +extension iPadMainViewController { + @objc func launchRocket() { + // rocketView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) + UIImageView.animate(withDuration: 4.4, animations: { + [weak self] in + self!.rocketView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) + self!.rocketView.frame.origin.y -= 1000 + }) { (done) in + UIImageView.animate(withDuration: 2.0, delay: 8.0, options: [.curveEaseOut], animations: { + [weak self] in + self!.rocketView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) + self!.rocketView.frame.origin.y += 1000 + }) + } + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/HumidityStackView.swift b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/HumidityStackView.swift new file mode 100644 index 0000000..ecbc947 --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/HumidityStackView.swift @@ -0,0 +1,83 @@ +/// +// iPadHumidityStack.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 11/5/23. +// + +import UIKit + + +class iPadHumidityStack: UIView { + // MARK: START: stuff needed for UIView + override init(frame: CGRect) { + super.init(frame: frame) +// createHumidityView() + setupUI() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + // MARK: END + + + // Topmost stack + let humidityView: UIStackView = { + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.axis = .vertical + stack.spacing = 15 + stack.backgroundColor = cyanColor + stack.layer.cornerRadius = 15 + stack.isLayoutMarginsRelativeArrangement = true + stack.layoutMargins = UIEdgeInsets(top: 10, left: 0, bottom: 40, right: 0) + return stack + }() + + let humidityTitleLabel: UILabel = { + let label = UILabel() + label.text = "Humidity" + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .boldSystemFont(ofSize: 18.0) + return label + }() + + let humidityLabel: UILabel = { + let label = UILabel() + label.text = "humidity" + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let dewPointLabel: UILabel = { + let label = UILabel() + label.text = "dew point" + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + // setsup the UI for the humidityStack + func setupUI() { + + humidityLabel.text = "\(WeatherKitData.Humidity)%" + humidityView.addArrangedSubview(humidityTitleLabel) + humidityView.addArrangedSubview(humidityLabel) + + addSubview(humidityView) + + // Setup constraints for humidityView to fill iPadHumidityStack + NSLayoutConstraint.activate([ + humidityView.topAnchor.constraint(equalTo: topAnchor), + humidityView.leadingAnchor.constraint(equalTo: leadingAnchor), + humidityView.trailingAnchor.constraint(equalTo: trailingAnchor), + humidityView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + func updateHumidityLabels(_ value: Int) { + humidityLabel.text = "\(value)%" + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/MainScrollView.swift b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/MainScrollView.swift new file mode 100644 index 0000000..19c5e65 --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/MainScrollView.swift @@ -0,0 +1,20 @@ +// +// MainScrollView.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 12/17/24. +// + +import Foundation +import UIKit + +extension iPadMainViewController { + func setupMainScrollView() { + //mainScrollView settings + mainScrollView.translatesAutoresizingMaskIntoConstraints = false + mainScrollView.contentSize = CGSize(width: 200, height: 300) + mainScrollView.alwaysBounceVertical = true + mainScrollView.isScrollEnabled = true + mainScrollView.alwaysBounceHorizontal = false + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/PrecipitationView.swift b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/PrecipitationView.swift new file mode 100644 index 0000000..9535df5 --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/PrecipitationView.swift @@ -0,0 +1,108 @@ +// +// PrecipitationView.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 11/14/23. +// + +import Foundation +import UIKit + +//extension iPadMainViewController { +// func createPrecipitationView() { +// precipitationView.translatesAutoresizingMaskIntoConstraints = false +// precipitationView.layer.cornerRadius = 15 +// precipitationView.axis = .vertical +// precipitationView.backgroundColor = cyanColor +// precipitationView.isLayoutMarginsRelativeArrangement = true +// precipitationView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 30, right: 0) +// precipitationView.spacing = 20 +// +// let PrecipitationViewTitleLabel: UILabel = { +// let label = UILabel() +// label.text = "Precipitation" +// label.textAlignment = .center +// label.font = .boldSystemFont(ofSize: 18.0) +// return label +// }() +// +// let PrecipitationChanceLabel: UILabel = { +// let label = UILabel() +// label.text = "--" +// label.textAlignment = .center +// return label +// }() +// +// PrecipitationChanceLabel.text = "\(WeatherKitData.PrecipitationChance)% Chance" +// +// precipitationView.addArrangedSubview(PrecipitationViewTitleLabel) +// precipitationView.addArrangedSubview(PrecipitationChanceLabel) +// } +//} + + +class iPadPrecipitationView: UIView { + // MARK: - Properties + private let precipitationStackView: UIStackView = { + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.layer.cornerRadius = 15 + stack.axis = .vertical + stack.backgroundColor = cyanColor + stack.isLayoutMarginsRelativeArrangement = true + stack.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 30, right: 0) + stack.spacing = 20 + return stack + }() + + private let PrecipitationViewTitleLabel: UILabel = { + let label = UILabel() + label.text = "Precipitation" + label.textAlignment = .center + label.font = .boldSystemFont(ofSize: 18.0) + return label + }() + + private let PrecipitationChanceLabel: UILabel = { + let label = UILabel() + label.text = "--" + label.textAlignment = .center + return label + }() + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupUI() + } + + // MARK: - Setup + private func setupUI() { + // Add labels to stack + precipitationStackView.addArrangedSubview(PrecipitationViewTitleLabel) + precipitationStackView.addArrangedSubview(PrecipitationChanceLabel) + + // Add stack to view + addSubview(precipitationStackView) + + // Setup constraints + NSLayoutConstraint.activate([ + precipitationStackView.topAnchor.constraint(equalTo: topAnchor), + precipitationStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + precipitationStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + precipitationStackView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + + updatePrecipitationLabel(WeatherKitData.PrecipitationChance) + } + + // MARK: - Public Methods + func updatePrecipitationLabel(_ value: Int) { + PrecipitationChanceLabel.text = "\(value)% Chance" + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/PressureView.swift b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/PressureView.swift new file mode 100644 index 0000000..945e3de --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/PressureView.swift @@ -0,0 +1,39 @@ +// +// PressureView.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 11/14/23. +// + +import Foundation +import UIKit + +extension iPadMainViewController { + func createPressureView() { + pressureView.translatesAutoresizingMaskIntoConstraints = false + pressureView.layer.cornerRadius = 15 + pressureView.axis = .vertical + pressureView.backgroundColor = cyanColor + pressureView.isLayoutMarginsRelativeArrangement = true + pressureView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 40, right: 0) + pressureView.spacing = 20 + + let PressureViewTitleLabel: UILabel = { + let label = UILabel() + label.text = "Pressure" + label.textAlignment = .center + label.font = .boldSystemFont(ofSize: 18.0) + return label + }() + + let PressureLabel: UILabel = { + let label = UILabel() + label.text = "--" + label.textAlignment = .center + return label + }() + + pressureView.addArrangedSubview(PressureViewTitleLabel) + pressureView.addArrangedSubview(PressureLabel) + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/SunsetSunriseView.swift b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/SunsetSunriseView.swift new file mode 100644 index 0000000..fef5988 --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/SunsetSunriseView.swift @@ -0,0 +1,132 @@ +// +// File.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 12/13/23. +// + +import Foundation +import UIKit + +//extension iPadMainViewController { +// func createSunsetView() { +// sunsetView.translatesAutoresizingMaskIntoConstraints = false +// sunsetView.layer.cornerRadius = 15 +// sunsetView.axis = .vertical +// sunsetView.spacing = 10 +// sunsetView.backgroundColor = cyanColor +// sunsetView.isLayoutMarginsRelativeArrangement = true +// sunsetView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0) +// +// +// let sunsetViewTitleLabel: UILabel = { +// let label = UILabel() +// label.text = "Sunset & Sunrise" +// label.textAlignment = .center +// label.font = .boldSystemFont(ofSize: 18.0) +// return label +// }() +// let sunriseLabel: UILabel = { +// let label = UILabel() +// label.text = "Sunrise: --" +// label.textAlignment = .center +// return label +// }() +// let sunsetLabel: UILabel = { +// let label = UILabel() +// label.text = "Sunset: --" +// label.textAlignment = .center +// return label +// }() +// let AMPMFormatter = DateFormatter() +// AMPMFormatter.dateFormat = "hh:mm a" +// +// sunriseLabel.text = "Sunrise: \(AMPMFormatter.string(from: WeatherKitData.Sunrise))" +// sunsetLabel.text = "Sunset: \(AMPMFormatter.string(from: WeatherKitData.Sunset))" +// +// sunsetView.addArrangedSubview(sunsetViewTitleLabel) +// sunsetView.addArrangedSubview(sunriseLabel) +// sunsetView.addArrangedSubview(sunsetLabel) +// } +//} + +class iPadSunsetView: UIView { + // MARK: - Properties + private let AMPMFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "hh:mm a" + return formatter + }() + + let sunsetView: UIStackView = { + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.layer.cornerRadius = 15 + stack.axis = .vertical + stack.spacing = 10 + stack.backgroundColor = cyanColor + stack.isLayoutMarginsRelativeArrangement = true + stack.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0) + return stack + }() + + let sunsetViewTitleLabel: UILabel = { + let label = UILabel() + label.text = "Sunset & Sunrise" + label.textAlignment = .center + label.font = .boldSystemFont(ofSize: 18.0) + return label + }() + + let sunriseLabel: UILabel = { + let label = UILabel() + label.text = "Sunrise: --" + label.textAlignment = .center + return label + }() + + let sunsetLabel: UILabel = { + let label = UILabel() + label.text = "Sunset: --" + label.textAlignment = .center + return label + }() + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupUI() + } + + // MARK: - Setup + func setupUI() { + // Add subviews to stack + sunsetView.addArrangedSubview(sunsetViewTitleLabel) + sunsetView.addArrangedSubview(sunriseLabel) + sunsetView.addArrangedSubview(sunsetLabel) + + // Add stack to view + addSubview(sunsetView) + + // Setup constraints for sunsetView to fill SunsetView + NSLayoutConstraint.activate([ + sunsetView.topAnchor.constraint(equalTo: topAnchor), + sunsetView.leadingAnchor.constraint(equalTo: leadingAnchor), + sunsetView.trailingAnchor.constraint(equalTo: trailingAnchor), + sunsetView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + + updateSunsetLabels(WeatherKitData.Sunrise, WeatherKitData.Sunset) + } + + // MARK: - Public Methods + func updateSunsetLabels(_ value1: Date, _ value2: Date) { + sunriseLabel.text = "Sunrise: \(AMPMFormatter.string(from: value1))" + sunsetLabel.text = "Sunset: \(AMPMFormatter.string(from: value2))" + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/UVView.swift b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/UVView.swift new file mode 100644 index 0000000..84da8ff --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/UVView.swift @@ -0,0 +1,209 @@ +// +// testExt.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 11/13/23. +// + +import Foundation +import UIKit + +//extension iPadMainViewController { +// +// func createUVView() { +// UVView.translatesAutoresizingMaskIntoConstraints = false +// UVView.layer.cornerRadius = 15 +// UVView.axis = .vertical +// UVView.backgroundColor = cyanColor +//// UVView.isLayoutMarginsRelativeArrangement = true +// UVView.isLayoutMarginsRelativeArrangement = true +// UVView.layoutMargins = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) +// UVView.spacing = 20 +// +// let UVViewTitleLabel: UILabel = { +// let label = UILabel() +// label.text = "UV Index" +// label.textAlignment = .center +// label.font = .boldSystemFont(ofSize: 18.0) +// return label +// }() +// +// let UVLabel: UILabel = { +// let label = UILabel() +// label.text = "--" +// label.textAlignment = .center +// return label +// }() +// +// uvProgressView.layer.addSublayer(uvGradientLayer) +// NSLayoutConstraint.activate([ +// uvProgressView.heightAnchor.constraint(equalToConstant: 6), +//// uvProgressView.leadingAnchor.constraint(equalTo: UVView.leadingAnchor, constant: 10), +//// uvProgressView.trailingAnchor.constraint(equalTo: UVView.trailingAnchor, constant: -10) +// ]) +// +// UVLabel.text = "\(WeatherKitData.UV)" +// UVView.addArrangedSubview(UVViewTitleLabel) +// UVView.addArrangedSubview(UVLabel) +// UVView.addArrangedSubview(uvProgressView) +// } +// +// // Stuff so that uvGradientLayer frame works +// override func viewDidLayoutSubviews() { +// super.viewDidLayoutSubviews() +// // Update gradient layer frame +// uvGradientLayer.frame = uvProgressView.bounds +// } +// +// // Function to update the UVIndex +// func updateUVIndex(_ value: Int) { +// // Create a mask layer to achieve the progress bar effect +// let maskLayer = CALayer() +// let clampedValue = min(max(value, 0), 11) +// let percentage = CGFloat(clampedValue) / 11.0 +// +// // The gradient layer should always be full width +// uvGradientLayer.frame = uvProgressView.bounds +// +// // Create a mask that only shows a portion of the gradient +// maskLayer.frame = CGRect(x: 0, y: 0, +// width: uvProgressView.bounds.width * percentage, +// height: uvProgressView.bounds.height) +// maskLayer.backgroundColor = UIColor.white.cgColor +// +// // Apply the mask to the gradient layer +// uvGradientLayer.mask = maskLayer +// } +//} + +class iPadUVView: UIView { + // MARK: - Properties + private let uvStackView: UIStackView = { + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.layer.cornerRadius = 15 + stack.axis = .vertical + stack.backgroundColor = cyanColor + stack.isLayoutMarginsRelativeArrangement = true + stack.layoutMargins = UIEdgeInsets(top: 0, left: 10, bottom: 10, right: 10) + stack.spacing = 6 + return stack + }() + + private let UVViewTitleLabel: UILabel = { + let label = UILabel() + label.text = "UV" + label.textAlignment = .center + label.heightAnchor.constraint(equalToConstant: 30).isActive = true + label.font = .boldSystemFont(ofSize: 18.0) + return label + }() + + private let UVLabel: UILabel = { + let label = UILabel() + label.text = "--" + label.numberOfLines = 2 + label.textAlignment = .left + return label + }() + + private let UVStatus: UILabel = { + let label = UILabel() + label.text = "--" + label.textAlignment = .center + label.heightAnchor.constraint(equalToConstant: 40).isActive = true + label.font = .boldSystemFont(ofSize: 32.0) + return label + }() + + private let uvProgressView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.cornerRadius = 4 + view.clipsToBounds = true + view.backgroundColor = UIColor.systemGray2.withAlphaComponent(0.3) + return view + }() + + private let uvGradientLayer: CAGradientLayer = { + let layer = CAGradientLayer() + layer.colors = [ + UIColor.systemGreen.cgColor, + UIColor.systemYellow.cgColor, + UIColor.systemOrange.cgColor, + UIColor.systemRed.cgColor, + UIColor.systemPurple.cgColor + ] + layer.startPoint = CGPoint(x: 0, y: 0.5) + layer.endPoint = CGPoint(x: 1, y: 0.5) + layer.cornerRadius = 2 + return layer + }() + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupUI() + } + + // MARK: - Setup + private func setupUI() { + // Add gradient layer to progress view + uvProgressView.layer.addSublayer(uvGradientLayer) + + // Add subviews to stack + uvStackView.addArrangedSubview(UVViewTitleLabel) + uvStackView.addArrangedSubview(UVStatus) + uvStackView.addArrangedSubview(uvProgressView) + uvStackView.addArrangedSubview(UVLabel) + + // Add stack to view + addSubview(uvStackView) + + // Setup constraints + NSLayoutConstraint.activate([ + uvStackView.topAnchor.constraint(equalTo: topAnchor), + uvStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + uvStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + uvStackView.bottomAnchor.constraint(equalTo: bottomAnchor), + + uvProgressView.heightAnchor.constraint(equalToConstant: 6) + ]) + + updateUVIndex(WeatherKitData.UV, WeatherKitData.UVCategory) + } + + override func layoutSubviews() { + super.layoutSubviews() + uvGradientLayer.frame = uvProgressView.bounds + } + + // MARK: - Public Methods + func updateUVIndex(_ value1: Int, _ value2: String) { + UVLabel.text = "The UV Index is currently \(value2)" + print("skibb: \(value2)") + UVStatus.text = "\(value1)" + + // Create a mask layer to achieve the progress bar effect + let maskLayer = CALayer() + let clampedValue = min(max(value1, 0), 11) + let percentage = CGFloat(clampedValue) / 11.0 + + // The gradient layer should always be full width + uvGradientLayer.frame = uvProgressView.bounds + + // Create a mask that only shows a portion of the gradient + maskLayer.frame = CGRect(x: 0, y: 0, + width: uvProgressView.bounds.width * percentage, + height: uvProgressView.bounds.height) + maskLayer.backgroundColor = UIColor.white.cgColor + + // Apply the mask to the gradient layer + uvGradientLayer.mask = maskLayer + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/WindView.swift b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/WindView.swift new file mode 100644 index 0000000..25f75dc --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/Components/WindView.swift @@ -0,0 +1,232 @@ +// +// WindView.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 11/14/23. +// + +import Foundation +import UIKit +import SwiftUI + + +class WindCompassView: UIView { + private let windSpeed: Double + private let windDirection: Double // in degrees + + init(frame: CGRect, windSpeed: Double, windDirection: Double) { + self.windSpeed = windSpeed + self.windDirection = windDirection + super.init(frame: frame) + backgroundColor = .clear + } + + required init?(coder: NSCoder) { + self.windSpeed = 0 + self.windDirection = 0 + super.init(coder: coder) + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { return } + + let center = CGPoint(x: bounds.midX, y: bounds.midY) + let radius = min(bounds.width, bounds.height) * 0.3 + + // Draw circle + context.setStrokeColor(UIColor.white.withAlphaComponent(0.3).cgColor) + context.setLineWidth(1) + context.addArc(center: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: false) + context.strokePath() + + // Draw tick marks + for i in 0..<360 { + if i % 30 == 0 { + let angle = CGFloat(i) * .pi / 180 + let startPoint = CGPoint( + x: center.x + (radius - 5) * cos(angle), + y: center.y + (radius - 5) * sin(angle) + ) + let endPoint = CGPoint( + x: center.x + radius * cos(angle), + y: center.y + radius * sin(angle) + ) + + context.move(to: startPoint) + context.addLine(to: endPoint) + context.strokePath() + } + } + + let directions = ["N", "E", "S", "W"] + let textMargin: CGFloat = 12 // Reduced from 20 + let positions = [ + CGPoint(x: center.x, y: center.y - radius - textMargin), + CGPoint(x: center.x + radius + textMargin, y: center.y), + CGPoint(x: center.x, y: center.y + radius + textMargin), + CGPoint(x: center.x - radius - textMargin, y: center.y) + ] + // Adjust font size based on view size + let fontSize = min(bounds.width, bounds.height) * 0.15 // Makes font size relative to view size + let attrs: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: fontSize), +// .font: customFont, + .foregroundColor: UIColor.white + ] + + // Adjust text positioning to account for font size + for (direction, position) in zip(directions, positions) { + let textSize = (direction as NSString).size(withAttributes: attrs) + let x = position.x - textSize.width / 2 + let y = position.y - textSize.height / 2 + (direction as NSString).draw(at: CGPoint(x: x, y: y), withAttributes: attrs) + } + + // Adjust center text size + let speedText = "\(Int(windSpeed))\nmph" + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + + let centerFontSize = min(bounds.width, bounds.height) * 0.2 // Makes center text size relative to view size + let centerAttrs: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: centerFontSize, weight: .medium), + .foregroundColor: UIColor.white, + .paragraphStyle: paragraphStyle + ] + + let textRect = CGRect(x: center.x - radius/2, y: center.y - radius/4, + width: radius, height: radius/2) + (speedText as NSString).draw(in: textRect, withAttributes: centerAttrs) + + // Draw arrow + context.saveGState() + context.translateBy(x: center.x, y: center.y) + context.rotate(by: CGFloat(windDirection) * .pi / 180) + + context.setFillColor(UIColor.white.cgColor) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(2) + + // Arrow body + // context.move(to: CGPoint(x: -radius + 10, y: 0)) + // context.addLine(to: CGPoint(x: radius - 10, y: 0)) + context.strokePath() + + // Instead of CGPath(polygonIn:), we'll draw the arrow head manually + // Arrow head + let arrowHeadLength: CGFloat = 16 + let arrowHeadWidth: CGFloat = 8 + + context.move(to: CGPoint(x: radius - 10, y: 0)) + context.addLine(to: CGPoint(x: radius - 10 - arrowHeadLength, y: -arrowHeadWidth)) + context.addLine(to: CGPoint(x: radius - 10 - arrowHeadLength, y: arrowHeadWidth)) + context.closePath() + context.fillPath() + + context.restoreGState() + } + + func setupFont() -> UIFont { + guard let customFont = UIFont(name: "SpaceX", size: 20.0) else { + fatalError(""" + Failed to load the "SpaceX" font. + Make sure the font file is included in the project and the font name is spelled correctly. + """ + ) + } + return customFont + } +} + + +class iPadWindView: UIView { + // MARK: - Properties + private let windStackView: UIStackView = { + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.layer.cornerRadius = 15 + stack.axis = .vertical + stack.backgroundColor = cyanColor + stack.isLayoutMarginsRelativeArrangement = true + stack.layoutMargins = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) + stack.spacing = 0 + return stack + }() + + private let WindViewTitleLabel: UILabel = { + let label = UILabel() + label.text = "Wind" + label.textAlignment = .center + label.font = .boldSystemFont(ofSize: 18.0) + return label + }() + + private let WindSpeedLabel: UILabel = { + let label = UILabel() + label.text = "--" + label.textAlignment = .center + return label + }() + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupUI() + } + + // create compass view + private var compassView: WindCompassView! + + // MARK: - Setup + private func setupUI() { + // Setup compass view + compassView = WindCompassView(frame: .zero, + windSpeed: Double(WeatherKitData.WindSpeed) ?? 0, + windDirection: (WeatherKitData.WindDirectionAngle - 90)) // true north is measured at 0 degrees, graphic starts at 90 though + compassView.translatesAutoresizingMaskIntoConstraints = false + + // Add labels to stack + windStackView.addArrangedSubview(WindViewTitleLabel) + + // Create a container view for the compass to control its size + let compassContainer = UIView() + compassContainer.translatesAutoresizingMaskIntoConstraints = false + compassContainer.addSubview(compassView) + + // Add compass container to stack + windStackView.addArrangedSubview(compassContainer) + // windStackView.addArrangedSubview(WindSpeedLabel) + // windStackView.addArrangedSubview(compassView) + + // Add stack to view + addSubview(windStackView) + + // Setup constraints + NSLayoutConstraint.activate([ + windStackView.topAnchor.constraint(equalTo: topAnchor), + windStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + windStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + windStackView.bottomAnchor.constraint(equalTo: bottomAnchor), + // Compass container constraints + compassContainer.heightAnchor.constraint(equalToConstant: 150), // Adjust this value + + // Compass view constraints + compassView.centerXAnchor.constraint(equalTo: compassContainer.centerXAnchor), + compassView.centerYAnchor.constraint(equalTo: compassContainer.centerYAnchor), + compassView.widthAnchor.constraint(equalToConstant: 120), // Adjust this value + compassView.heightAnchor.constraint(equalToConstant: 120) // Adjust this value + ]) + + updateWindLabel(WeatherKitData.WindSpeed) + } + + // MARK: - Public Methods + func updateWindLabel(_ value: String) { + WindSpeedLabel.text = "\(value)" + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/iPadDailyCollectionViewCell/iPadDailyCollectionViewCell.swift b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/iPadDailyCollectionViewCell/iPadDailyCollectionViewCell.swift new file mode 100644 index 0000000..2ab8e47 --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/iPadDailyCollectionViewCell/iPadDailyCollectionViewCell.swift @@ -0,0 +1,115 @@ +// +// iPadDailyCollectionViewCell.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 11/19/23. +// + +import UIKit + +class iPadDailyCollectionViewCell: UICollectionViewCell { + // Add UI elements as needed + // topStack is needed so that stackview doesn't slide around + var topStack: UIStackView! +// var stackView: UIStackView! + var minMaxStack: UIStackView! + var dayOfWeek: UILabel! + var weatherIcon: UIImageView! + var maxTempLabel: UILabel! + var minTempLabel: UILabel! + var TTT: UIStackView! + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + } + + private func setupUI() { + // cyanColor for views in mainview + let cyanColor = UIColor(red: 95.0/255.0, green: 135.0/255.0, blue: 255.0/255.0, alpha: 0.93) + + topStack = UIStackView() + minMaxStack = UIStackView() + maxTempLabel = UILabel() + minTempLabel = UILabel() + dayOfWeek = UILabel() + weatherIcon = UIImageView() + + topStack.translatesAutoresizingMaskIntoConstraints = false +// topStack.backgroundColor = .orange + topStack.backgroundColor = cyanColor + topStack.applyBlurEffect(cornerRadius: 15) +// topStack.layer.opacity = 0.5 +// topStack.isOpaque = true + topStack.axis = .vertical + topStack.layer.cornerRadius = 15 + + weatherIcon.translatesAutoresizingMaskIntoConstraints = false + weatherIcon.image = UIImage(systemName: "sun.max.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20.0))?.withRenderingMode(.alwaysOriginal) + weatherIcon.contentMode = .scaleAspectFit + + minMaxStack.translatesAutoresizingMaskIntoConstraints = false + minMaxStack.axis = .horizontal + minMaxStack.spacing = 1 + + maxTempLabel.translatesAutoresizingMaskIntoConstraints = false + maxTempLabel.text = "Max" + + minTempLabel.translatesAutoresizingMaskIntoConstraints = false + minTempLabel.text = "Min" + + dayOfWeek = UILabel() + dayOfWeek.translatesAutoresizingMaskIntoConstraints = false + dayOfWeek.text = "WeekDay" + dayOfWeek.textAlignment = .center + + topStack.addArrangedSubview(weatherIcon) + topStack.addArrangedSubview(minMaxStack) + topStack.addArrangedSubview(dayOfWeek) + minMaxStack.addArrangedSubview(minTempLabel) + minMaxStack.addArrangedSubview(maxTempLabel) + contentView.addSubview(topStack) + + // we set up the font for the labels + let customFont: UIFont + customFont = setupFont() + + minTempLabel.font = customFont + maxTempLabel.font = customFont + minTempLabel.adjustsFontSizeToFitWidth = true + maxTempLabel.adjustsFontSizeToFitWidth = true + + NSLayoutConstraint.activate([ +// contentView.heightAnchor.constraint(equalToConstant: 200), + topStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), + topStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5), + topStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5), + topStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5), + // minMaxStack constraints +// weatherIcon.heightAnchor.constraint(equalToConstant: 60), +// weatherIcon.widthAnchor.constraint(equalToConstant: 10), + minMaxStack.heightAnchor.constraint(equalToConstant: 60), + dayOfWeek.heightAnchor.constraint(equalToConstant: 40), +// minMaxStack.topAnchor.constraint(equalTo: topStack.topAnchor), +// minMaxStack.bottomAnchor.constraint(equalTo: topStack.bottomAnchor), +// minMaxStack.leadingAnchor.constraint(equalTo: topStack.leadingAnchor), +// minMaxStack.trailingAnchor.constraint(equalTo: topStack.trailingAnchor), + ]) + } + + func setupFont() -> UIFont { + guard let customFont = UIFont(name: "SpaceX", size: 20.0) else { + fatalError(""" + Failed to load the "SpaceX" font. + Make sure the font file is included in the project and the font name is spelled correctly. + """ + ) + } + return customFont + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/iPadMainViewController.swift b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/iPadMainViewController.swift new file mode 100644 index 0000000..21d1004 --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/MainViewController/iPadMainViewController.swift @@ -0,0 +1,498 @@ +// +// iPadMainViewController.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 10/31/23. +// + +import Foundation +import UIKit +import CoreLocation + +let cyanColor = UIColor(red: 95.0/255.0, green: 195.0/255.0, blue: 255.0/255.0, alpha: 0.93) + +class iPadMainViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, CLLocationManagerDelegate { + //Creates the location manager + let locationManager = CLLocationManager() + + // MARK: Top Current Stack Component + let customView = iPadMainTopCurrentStack() + let mainScrollView = UIScrollView() + let humidityView = iPadHumidityStack() +// let humidityView = UIStackView() + let rocketView = UIImageView() +// let sunsetView = UIStackView() + let sunsetView = iPadSunsetView() +// let UVView = UIStackView() + let UVView = iPadUVView() +// let windView = UIStackView() + let windView = iPadWindView() +// let precipitationView = UIStackView() + let precipitationView = iPadPrecipitationView() + let pressureView = UIStackView() + //Creates view for cloud at top + let cloudViewHolder = UIView() + //Creates a refresh control for the scrollview + var refreshControl = UIRefreshControl() + + + // collection view for hourly forecast + let hourlyForecastView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout.init()) // need to have frame and layout for UICollectionView + + // collection view for daily forecast + let dailyForecastView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout.init()) + + // cyanColor for views in mainview +// let cyanColor = UIColor(red: 95.0/255.0, green: 195.0/255.0, blue: 255.0/255.0, alpha: 0.93) + + // our gradient progress bar for uv view + let uvProgressView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.cornerRadius = 4 + view.clipsToBounds = true + view.backgroundColor = UIColor.systemGray2.withAlphaComponent(0.3) // Add this line + return view + }() + + let uvGradientLayer: CAGradientLayer = { + let layer = CAGradientLayer() + layer.colors = [ + UIColor.systemGreen.cgColor, + UIColor.systemYellow.cgColor, + UIColor.systemOrange.cgColor, + UIColor.systemRed.cgColor, + UIColor.systemPurple.cgColor + ] + layer.startPoint = CGPoint(x: 0, y: 0.5) + layer.endPoint = CGPoint(x: 1, y: 0.5) + layer.cornerRadius = 2 // Add this line to match the view's corner radius + return layer + }() + + + override func viewDidLoad() { + super.viewDidLoad() + // Ask for Authorisation from the User. + self.locationManager.requestAlwaysAuthorization() + // For use in foreground + self.locationManager.requestWhenInUseAuthorization() + // sets SVG background + setBackground() + setupRocketView() + // create humidityView +// createHumidityView() + // create sunsetView +// createSunsetView() + // create UVView +// createUVView() + // create precipitationView +// createPrecipitationView() + // create windView +// createWindView() + // create pressureView + createPressureView() + // sets up the UI + setupUI() + // configure the hourly forecast view + configureCollectionView() + // + setupMainScrollView() + + //Sets settings for refreshControl + refreshControl.translatesAutoresizingMaskIntoConstraints = false + refreshControl.attributedTitle = NSAttributedString("Fetching Weather") + refreshControl.addTarget(self, action: #selector(refresh(sender:)), for: UIControl.Event.valueChanged) + } + + // sets up the RocketView to the right + private func setupRocketView() { + //rocketView settings + let rocketTapGesture = UITapGestureRecognizer(target: self, action: #selector(launchRocket)) + rocketTapGesture.isEnabled = true + rocketView.translatesAutoresizingMaskIntoConstraints = false + rocketView.contentMode = .scaleAspectFit + rocketView.clipsToBounds = true + rocketView.image = UIImage(named: "Falcon-Heavy.svg") + rocketView.layer.shadowColor = UIColor.black.cgColor + rocketView.layer.shadowRadius = 15.0 + rocketView.layer.shadowOpacity = 0.7 + rocketView.layer.shadowOffset = CGSize(width: -4, height: 4) + rocketView.layer.masksToBounds = false + rocketView.isUserInteractionEnabled = true + rocketView.addGestureRecognizer(rocketTapGesture) + } + + private func setupUI() { + customView.translatesAutoresizingMaskIntoConstraints = false + humidityView.translatesAutoresizingMaskIntoConstraints = false + rocketView.translatesAutoresizingMaskIntoConstraints = false + hourlyForecastView.translatesAutoresizingMaskIntoConstraints = false + dailyForecastView.translatesAutoresizingMaskIntoConstraints = false + sunsetView.translatesAutoresizingMaskIntoConstraints = false + precipitationView.translatesAutoresizingMaskIntoConstraints = false + windView.translatesAutoresizingMaskIntoConstraints = false + UVView.translatesAutoresizingMaskIntoConstraints = false + + //Sets settings for refreshControl + refreshControl.translatesAutoresizingMaskIntoConstraints = false + refreshControl.attributedTitle = NSAttributedString("Fetching Weather") + refreshControl.addTarget(self, action: #selector(refresh(sender:)), for: UIControl.Event.valueChanged) + + + view.backgroundColor = .orange + +// mainScrollView.addSubview(customView) + mainScrollView.addSubview(humidityView) +// mainScrollView.addSubview(rocketView) + mainScrollView.addSubview(hourlyForecastView) + mainScrollView.addSubview(dailyForecastView) + mainScrollView.addSubview(sunsetView) + mainScrollView.addSubview(UVView) + mainScrollView.addSubview(precipitationView) + mainScrollView.addSubview(windView) + mainScrollView.addSubview(refreshControl) + + view.addSubview(rocketView) + view.addSubview(customView) + view.addSubview(mainScrollView) + + + // activates constraints + NSLayoutConstraint.activate([ + //customView constraints + customView.topAnchor.constraint(equalTo: view.topAnchor), + customView.heightAnchor.constraint(equalToConstant: 100), + customView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + customView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + //topStack constraints + customView.topStack.topAnchor.constraint(equalTo: customView.topAnchor, constant: 20), + customView.topStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + customView.topStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 250), + //mainScrollView constraints + mainScrollView.topAnchor.constraint(equalTo: customView.topStack.bottomAnchor, constant: 20), + mainScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 250), + mainScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mainScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + //humidityView constraints + humidityView.topAnchor.constraint(equalTo: mainScrollView.topAnchor, constant: 15), + humidityView.heightAnchor.constraint(equalToConstant: 100), + humidityView.leadingAnchor.constraint(equalTo: mainScrollView.leadingAnchor), + humidityView.widthAnchor.constraint(equalToConstant: 180), + //rocketView constraints + rocketView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + rocketView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5), + rocketView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + rocketView.trailingAnchor.constraint(equalTo: mainScrollView.leadingAnchor, constant: 0), + // hourly forecast view constraints + hourlyForecastView.topAnchor.constraint(equalTo: mainScrollView.topAnchor, constant: 15), + hourlyForecastView.leadingAnchor.constraint(equalTo: humidityView.trailingAnchor, constant: 15), + hourlyForecastView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hourlyForecastView.heightAnchor.constraint(equalToConstant: 100), + // daily forecast view constraints + dailyForecastView.topAnchor.constraint(equalTo: hourlyForecastView.bottomAnchor, constant: 15), + dailyForecastView.leadingAnchor.constraint(equalTo: hourlyForecastView.leadingAnchor), + dailyForecastView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + dailyForecastView.bottomAnchor.constraint(equalTo: precipitationView.bottomAnchor), + // sunsetView constraints + sunsetView.topAnchor.constraint(equalTo: humidityView.bottomAnchor, constant: 15), + sunsetView.leadingAnchor.constraint(equalTo: humidityView.leadingAnchor), + sunsetView.trailingAnchor.constraint(equalTo: humidityView.trailingAnchor), + sunsetView.heightAnchor.constraint(equalToConstant: 100), + // UVView constraints + UVView.topAnchor.constraint(equalTo: dailyForecastView.bottomAnchor, constant: 15), + UVView.leadingAnchor.constraint(equalTo: dailyForecastView.leadingAnchor), + UVView.widthAnchor.constraint(equalToConstant: 180), + UVView.heightAnchor.constraint(equalToConstant: 180), + // precipitationView constraints + precipitationView.topAnchor.constraint(equalTo: sunsetView.bottomAnchor, constant: 15), + precipitationView.leadingAnchor.constraint(equalTo: sunsetView.leadingAnchor), + precipitationView.trailingAnchor.constraint(equalTo: sunsetView.trailingAnchor), + precipitationView.heightAnchor.constraint(equalToConstant: 100), + // windView constraints + windView.topAnchor.constraint(equalTo: dailyForecastView.bottomAnchor, constant: 15), + windView.leadingAnchor.constraint(equalTo: precipitationView.leadingAnchor), + windView.widthAnchor.constraint(equalToConstant: 180), + windView.heightAnchor.constraint(equalToConstant: 180), + ]) + } + + + func setBackground() { + //Creates cloud at top of the screen + cloudViewHolder.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height / 4) + let cloud = UIImage(named: "Cloud.svg") + let cloudView : UIImageView! + cloudView = UIImageView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height / 4)) + cloudView.contentMode = .scaleAspectFit + cloudView.layer.opacity = 0.6 + cloudView.clipsToBounds = true + cloudView.image = cloud + cloudView.center = view.center + cloudView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height / 4) + cloudViewHolder.addSubview(cloudView) + view.sendSubviewToBack(cloudViewHolder) + + + let background = UIImage(named: "Background.svg") + var imageView : UIImageView! + imageView = UIImageView(frame: view.bounds) + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + imageView.image = background + imageView.center = view.center + + // add subview here so that layout anchors have view to constrain to + view.addSubview(imageView) + imageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(lessThanOrEqualToSystemSpacingBelow: view.topAnchor, multiplier: 1.05), + imageView.bottomAnchor.constraint(equalToSystemSpacingBelow: view.bottomAnchor, multiplier: 1.05), + imageView.leadingAnchor.constraint(equalToSystemSpacingAfter: view.leadingAnchor, multiplier: -1.05), + imageView.trailingAnchor.constraint(equalToSystemSpacingAfter: view.trailingAnchor, multiplier: 1.05) + ]) + view.sendSubviewToBack(imageView) + + // horizontal parallax effects + let horizontalMotionEffect = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis) + horizontalMotionEffect.minimumRelativeValue = -20 + horizontalMotionEffect.maximumRelativeValue = 20 + + // vertical parallax effects + let verticalMotionEffect = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis) + verticalMotionEffect.minimumRelativeValue = -20 + verticalMotionEffect.maximumRelativeValue = 20 + + // adds horizontal and vertical to "motionEffectGroup" which is then added to imageview + let motionEffectGroup = UIMotionEffectGroup() + motionEffectGroup.motionEffects = [horizontalMotionEffect, verticalMotionEffect] + imageView.addMotionEffect(motionEffectGroup) + } + + + //MARK: Hourly Forecast Collection View + private func configureCollectionView() { + hourlyForecastView.delegate = self + hourlyForecastView.dataSource = self + dailyForecastView.delegate = self + dailyForecastView.dataSource = self + + // stylistic options for hourly view + hourlyForecastView.layer.cornerRadius = 15 + hourlyForecastView.backgroundColor = cyanColor + hourlyForecastView.showsHorizontalScrollIndicator = false + + // stylistic options for daily view + dailyForecastView.layer.cornerRadius = 15 + dailyForecastView.backgroundColor = cyanColor + dailyForecastView.showsHorizontalScrollIndicator = false + + // Register your custom cell class + hourlyForecastView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell") +// Set the collection view's layout to horizontal scroll + if let layout2 = hourlyForecastView.collectionViewLayout as? UICollectionViewFlowLayout { + layout2.scrollDirection = .horizontal + } + + dailyForecastView.register(iPadDailyCollectionViewCell.self, forCellWithReuseIdentifier: "iPadDailyCollectionViewCell") + if let layout3 = dailyForecastView.collectionViewLayout as? UICollectionViewFlowLayout { + layout3.itemSize = CGSize(width: 150, height: 215) + layout3.scrollDirection = .horizontal + } + } + + // number of hourly cells + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { +// if hourlyForecastView, then we have 16 cells, otherwise 10 + if collectionView == self.hourlyForecastView { + return 15 + } + else { return 7 } + } + + // populates hourly cells + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + // use hourly cell if hourlyForecastView + if collectionView == self.hourlyForecastView { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell + cell.tempLabel.text = "--" + let currentHour = getCurrentHour(offset: indexPath.row) + cell.timeLabel.text = " \(currentHour) " // added some spaces so that cell would be wider + cell.timeLabel.font = .systemFont(ofSize: 16.0) + + // checks if there is enough data to show + if WeatherKitData.HourlyForecastSymbol.count > 15 { + //simple check for when icon is "wind" (doesn't have "fill" option) + if WeatherKitData.HourlyForecastSymbol[indexPath.row] != "wind" { + cell.weatherIcon.image = UIImage(systemName: "\(WeatherKitData.HourlyForecastSymbol[indexPath.row]).fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 32.0))?.withRenderingMode(.alwaysOriginal) + } else { + cell.weatherIcon.image = UIImage(systemName: "\(WeatherKitData.HourlyForecastSymbol[indexPath.row])", withConfiguration: UIImage.SymbolConfiguration(pointSize: 32.0))?.withRenderingMode(.alwaysOriginal) + } + } else { cell.weatherIcon.image = UIImage(systemName: "questionmark")} + + // checks if there is enough data for hourly forecast + if WeatherKitData.HourlyForecast.count > 15 { + cell.tempLabel.text = "\(Int((round(WeatherKitData.HourlyForecast[indexPath.row])*100)/100))˚" + } else { cell.tempLabel.text = "--" } + + return cell + } + +// else use daily cell (for dailyForecastView) + else { + let cell2 = collectionView.dequeueReusableCell(withReuseIdentifier: "iPadDailyCollectionViewCell", for: indexPath) as! iPadDailyCollectionViewCell + NSLayoutConstraint.activate([ + cell2.heightAnchor.constraint(equalToConstant: 100) + ]) + + print("IndexPath: \(indexPath.row)") + if WeatherKitData.TempMinForecast.count > 5 { + cell2.minTempLabel.text = "\(WeatherKitData.TempMinForecast[indexPath.row])" + } else { + cell2.minTempLabel.text = "--" + } + + if WeatherKitData.TempMaxForecast.count > 5{ + cell2.maxTempLabel.text = "\(WeatherKitData.TempMaxForecast[indexPath.row])" + } else { + cell2.maxTempLabel.text = "--" + } + + // checks if there is enough data to show + if WeatherKitData.forecastSymbol.count > 6 { + //simple check for when icon is "wind" (doesn't have "fill" option) + if WeatherKitData.forecastSymbol[indexPath.row] != "wind" { + cell2.weatherIcon.image = UIImage(systemName: "\(WeatherKitData.forecastSymbol[indexPath.row]).fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 32.0))?.withRenderingMode(.alwaysOriginal) + } else { + cell2.weatherIcon.image = UIImage(systemName: "\(WeatherKitData.forecastSymbol[indexPath.row])", withConfiguration: UIImage.SymbolConfiguration(pointSize: 32.0))?.withRenderingMode(.alwaysOriginal) + } + } else { cell2.weatherIcon.image = UIImage(systemName: "questionmark")} + print("Cl: \(Calendar.current.weekdaySymbols)") + + var Day = getCurrentDayOfWeek(offset: indexPath.row) + Day = Day.localizedCapitalized + cell2.dayOfWeek.text = Day + + return cell2 + } + } + //MARK: - Function for the pull to refresh on the scrollview + @objc func refresh(sender:AnyObject) { + // Code to refresh table view + print("userLatitude: \((UserLocation.userLatitude)!)") + print("userLongitude: \((UserLocation.userLongitude)!)") + DispatchQueue.main.async { + self.refreshControl.endRefreshing() + self.fetchFromReload() + } + } + + //Creates a function for running fetchWeather from reload func + func fetchFromReload() { + DispatchQueue.main.async { + if UserLocation.userCLLocation != nil { + WeatherKitData.HourlyForecast.removeAll() + WeatherKitData.TempMaxForecast.removeAll() + WeatherKitData.TempMinForecast.removeAll() + self.getWeather(location: UserLocation.userCLLocation!) + } else { + } + self.updateLabels() + self.hourlyForecastView.reloadData() + self.dailyForecastView.reloadData() + } + } + + //MARK: - Function to refresh all of the labels + func updateLabels() { + //Checks if weatherkit returned symbol is "wind" + //This is because "wind" (SF Symbol) has no fill option + /* + if WeatherKitData.Symbol != "wind" { + self.iconView.image = UIImage(systemName: WeatherKitData.Symbol + ".fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 64.0))?.withRenderingMode(.alwaysOriginal) + } else { + self.iconView.image = UIImage(systemName: WeatherKitData.Symbol, withConfiguration: UIImage.SymbolConfiguration(pointSize: 64.0))?.withRenderingMode(.alwaysOriginal) + } + */ + self.customView.currentTempLabel.text = "Current: \(WeatherKitData.Temp)" + self.customView.maxTempLabel.text = "High: \(WeatherKitData.TempMax)" + self.customView.minTempLabel.text = "Low: \(WeatherKitData.TempMin)" + self.humidityView.updateHumidityLabels(WeatherKitData.Humidity) + self.precipitationView.updatePrecipitationLabel(WeatherKitData.PrecipitationChance) + self.sunsetView.updateSunsetLabels(WeatherKitData.Sunrise, WeatherKitData.Sunset) + self.windView.updateWindLabel(WeatherKitData.WindSpeed) + self.UVView.updateUVIndex(WeatherKitData.UV, WeatherKitData.UVCategory) +// self.windLabel.text = "\(WeatherKitData.WindSpeed)" +// self.precipitationLabel.text = "\(WeatherKitData.PrecipitationChance)% Chance" + DateConverter().timeArrayMaker() + } + + func getCurrentDayOfWeek(offset: Int) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEEE" // "EEEE" will give you the full day name like "Monday", "Tuesday", etc. + + var currentDate = Date() + + let daysToAdd = offset + if let newDate = Calendar.current.date(byAdding: .day, value: daysToAdd, to: currentDate) { + currentDate = newDate + } + + let dayOfWeekString = dateFormatter.string(from: currentDate) + + return dayOfWeekString + } + func getCurrentHour(offset: Int) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "h a" // "EEEE" will give you the full day name like "Monday", "Tuesday", etc. + + var currentHour = Date() + + let hoursToAdd = offset + if let newHour = Calendar.current.date(byAdding: .hour, value: hoursToAdd, to: currentHour) { + currentHour = newHour + } + + let currentHourString = dateFormatter.string(from: currentHour) + + return currentHourString + } + + //MARK: - Function to refresh all of the labels +// func updateLabels() { +// self.customView.currentTempLabel.text = "\(WeatherKitData.Temp)" +// self.customView.minTempLabel.text = "Low Temp:\(WeatherKitData.TempMin)" +// self.customView.maxTempLabel.text = "High Temp:\(WeatherKitData.TempMax)" +// } + + //Function for checking location manager status (starts getting the location if allowed) + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + let status = manager.authorizationStatus + + switch status { + case .notDetermined: + print("status: notDetermined") + case .denied: + customView.currentCityLabel.text = "No Location!" + print("status: denied") + case .restricted: + customView.currentCityLabel.text = "No Location!" + print("status: restricted") + case .authorizedAlways: + print("status: authorizedAlways") + locationManager.startUpdatingLocation() +// viewDidLoadRefresh() + if UserLocation.userCLLocation != nil { + getWeather(location: UserLocation.userCLLocation!) + } + case .authorizedWhenInUse: + print("status: authorizedWhenInUse") + locationManager.startUpdatingLocation() + +// viewDidLoadRefresh() + default: + print("unknown ") + } + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPad View Controllers/Utils/iPadWeatherKitCallUtil.swift b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/Utils/iPadWeatherKitCallUtil.swift new file mode 100644 index 0000000..98bdfee --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPad View Controllers/Utils/iPadWeatherKitCallUtil.swift @@ -0,0 +1,201 @@ +// +// iPadWeatherKitCallUtil.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 11/20/23. +// + +import Foundation +import WeatherKit +import CoreLocation +import UIKit + + +//let weatherService = WeatherService() + +extension iPadMainViewController { + func getWeather(location: CLLocation) { + Task{ + do { + //fetches location + UserLocation.userCLLocation?.fetchCityAndCountry(completion: { city, country, error in + guard let city = city, let country = country, error == nil else { return } + print(city + ", " + country) // City, Country + //Puts city name into cityLabel +// self.cityLabel.text = "\(city)" + self.customView.currentCityLabel.text = "\(city)" +// self.cityLabel.text = "San Francisco" + }) +// let TempLocation = CLLocation(latitude: 39.9042, longitude: 116.4074) // Beijing + + let calendar = Calendar.current + let endDate = calendar.date(byAdding: .hour, value: 12,to: Date.now) + let result = try await weatherService.weather(for: location, including: .current, .hourly(startDate: Date.now, endDate: endDate!), .daily) + + let dateFormatter = DateFormatter() + dateFormatter.timeStyle = DateFormatter.Style.short + dateFormatter.dateStyle = DateFormatter.Style.none + dateFormatter.timeZone = .current + + // will have 0 maximumFractionDigits + let MF0 = MeasurementFormatter() + MF0.unitStyle = .short + MF0.locale = .autoupdatingCurrent + MF0.numberFormatter.maximumFractionDigits = 0 + + // will have 1 maximumFractionDigits + let MF1 = MeasurementFormatter() + MF1.unitStyle = .short + MF1.locale = .autoupdatingCurrent + MF1.numberFormatter.maximumFractionDigits = 1 + + //Data from currentWeather + let temp = MF0.string(from: result.0.temperature) + + MF0.numberFormatter.maximumFractionDigits = 3 + let uv = result.0.uvIndex.value + let uvCategory = result.0.uvIndex.category.description + let windSpeed = MF0.string(from: result.0.wind.speed) + print(windSpeed) + let windDirection = result.0.wind.compassDirection + let windDirectionAngle = result.0.wind.direction.converted(to: .degrees) + print("gust: \(result.0.wind.gust)") + let symbol = result.0.symbolName + let humidity = Int(100 * result.0.humidity) + let pressure = MF1.string(from: result.0.pressure) + + //Data from dailyForecast[0] (today) + let tempMax = MF0.string(from: result.2[0].highTemperature) + let tempMin = MF0.string(from: result.2[0].lowTemperature) + let localSunrise = dateFormatter.string(from: result.2.forecast[0].sun.sunrise!) + let localSunset = dateFormatter.string(from: result.2.forecast[0].sun.sunset!) + let solarNoon = dateFormatter.string(from: result.2.forecast[0].sun.solarNoon!) + let astronomicalDawn = dateFormatter.string(from: result.2[0].sun.astronomicalDawn!) + let astronomicalDusk = dateFormatter.string(from: result.2[0].sun.astronomicalDusk!) + + var rainChance = Int(100 * result.2.forecast[0].precipitationChance) + if rainChance < 15 { + rainChance = 0 + } else {} + + //For loop for the tempMax for 5 days + for i in 0...6 { +// print(result.dailyForecast[i].highTemperature) + let maxTemp = result.2[i].highTemperature + WeatherKitData.TempMaxForecast.append(MF0.string(from: maxTemp)) //Append is needed to append into array + print("TempMaxForecast: \(WeatherKitData.TempMaxForecast[i])") + let forecastSymbol = result.2[i].symbolName + WeatherKitData.forecastSymbol.append(forecastSymbol) + print("forecastSymbol: \(WeatherKitData.forecastSymbol[i])") + let minTemp = result.2[i].lowTemperature + WeatherKitData.TempMinForecast.append(MF0.string(from: minTemp)) + print("TempMinForecast: \(WeatherKitData.TempMinForecast[i])") +// print("WEATHERKITDATA TempMax array: \(WeatherKitData.TempMaxForecast[i])") + } + + //For loop for 12hours + for i in 0...11 { + // wind speed for next 12 hours + let formatter = MeasurementFormatter() + formatter.unitOptions = .temperatureWithoutUnit + let windSpeed = result.1.forecast[i].wind.speed + let wind = (round(windSpeed.value * 10)) / 10 +// print("Wind: \(wind)") + WeatherKitData.WindSpeedForecast.append(wind) + + // gusts for next 12 hours` + let gust = result.1.forecast[i].wind.gust + if let unwrapperGust = gust { + WeatherKitData.WindGusts.append(unwrapperGust) + print("gust#\(i): \(unwrapperGust)") + } + + // precipitation chance for next 12 hours + print("precipitation chance\(i): \(result.1.forecast[i].precipitationChance)") + // doesn't need to be unwrapped because "...forecast[i].precipitationChance" is not optional + let precipitationChance = result.1.forecast[i].precipitationChance + WeatherKitData.PrecipitationChanceForecast.append(precipitationChance * 100) // *100 is needed because precipitationChance is from 0-1 + + + } + print("PrecipitatiomnChanceForecast: \(WeatherKitData.PrecipitationChanceForecast)") + + //For loop for 12 hour weather + for i in 0...11 { + let forecast = result.1.forecast[i].temperature.value + WeatherKitData.HourlyForecast.append(forecast) + print("Hourly Forecast: \(WeatherKitData.HourlyForecast[i])") + let symbol = result.1.forecast[i].symbolName + WeatherKitData.HourlyForecastSymbol.append(symbol) + } + +// print(temp) +// print(uv) +// print(symbol) +// print(rainChance) +// print(result.1.forecast[0].wind) +// if result.alerts!.count > 0 { +// print("Weather Alert: \(result.weatherAlerts?[0].summary)") +// } else { +// print("No alerts") +// } + + print(Double(WeatherKitData.WindSpeedForecast[1])) + //Puts fetched data into WeatherKitData struct + WeatherKitData.Temp = temp + WeatherKitData.TempMax = tempMax + WeatherKitData.TempMin = tempMin + WeatherKitData.UV = uv + WeatherKitData.UVCategory = uvCategory + WeatherKitData.WindSpeed = windSpeed + WeatherKitData.WindDirection = "\(windDirection)" + WeatherKitData.WindDirectionAngle = windDirectionAngle.value + WeatherKitData.Symbol = symbol + WeatherKitData.Humidity = humidity + WeatherKitData.Symbol = symbol + WeatherKitData.Sunrise = result.2.forecast[0].sun.sunrise! + WeatherKitData.Sunset = result.2.forecast[0].sun.sunset! + WeatherKitData.localSunrise = localSunrise + WeatherKitData.localSunset = localSunset + WeatherKitData.SolarNoon = solarNoon + WeatherKitData.AstronomicalDawn = astronomicalDawn + WeatherKitData.AstronomicalDusk = astronomicalDusk + WeatherKitData.Pressure = pressure + WeatherKitData.PrecipitationChance = rainChance + + + WeatherKitData.SunriseDate = result.2.forecast[0].sun.sunrise! + WeatherKitData.SunsetDate = result.2.forecast[0].sun.sunset! + + + DateConverter().timeArrayMaker() //Runs timeArrayMaker func for timeArray in widget + //puts WidgetData struct into widget + var widget = WidgetData(temp: temp, tempMax: tempMax, tempMin: tempMin, symbolName: symbol, hourlyForecast: WeatherKitData.HourlyForecast, forecastTimeArray: timeArray.formattedHours) + let primaryData = PrimaryData(widgetData: widget) + //Encodes data into AppGroup + primaryData.encode() + + } catch { + print(String(describing: error)) + } + self.updateLabelsAfterAwait() + } + } + //@MainActor runs after getweather Task completes + //@MainActor ensures all code runs on main dispatch thread + @MainActor + private func updateLabelsAfterAwait() { + ForecastListVC().forecastTableView.reloadData() + print("updateLabelsAfterAwait run") +// getWeatherLabelUpdate() + DateConverter().convertDateToEpoch() + createNotification() + + let date = WeatherKitData.SunsetDate + let interval = WeatherKitData.SunsetDate.timeIntervalSinceReferenceDate - WeatherKitData.SunriseDate.timeIntervalSinceReferenceDate + print("interval time: \(interval)") + +// let rocketTimer = Timer(fireAt: date, interval: interval, target: self, selector: #selector(AnimateRocket), userInfo: nil, repeats: false) +// RunLoop.main.add(rocketTimer, forMode: RunLoop.Mode.common) + } +} diff --git a/programmatic-ui-weather-app/iPad Version/iPadMainTopCurrentStack/iPadMainTopCurrentStack.swift b/programmatic-ui-weather-app/iPad Version/iPadMainTopCurrentStack/iPadMainTopCurrentStack.swift new file mode 100644 index 0000000..11bacdc --- /dev/null +++ b/programmatic-ui-weather-app/iPad Version/iPadMainTopCurrentStack/iPadMainTopCurrentStack.swift @@ -0,0 +1,113 @@ +// +// iPadMainTopCurrentStack.swift +// weatherkit-weather-app +// +// Created by Ruslan Spirkin on 11/1/23. +// + +import UIKit + +class iPadMainTopCurrentStack: UIView { + + // MARK: START: stuff needed for UIView + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + // MARK: END + + + // Topmost stack + let topStack: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 15 + return stack + }() + + // Current City Label + let currentCityLabel: UILabel = { + let label = UILabel() + label.text = "No Location!" + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + // Top Info Stack + let topInfoStack: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.spacing = 15 + stack.distribution = .equalCentering + return stack + }() + + // Min Temperature Label + let minTempLabel: UILabel = { + let label = UILabel() + label.text = "--" + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + // Current Temp Label + let currentTempLabel: UILabel = { + let label = UILabel() + label.text = "--" + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + // Max Temperature Label + let maxTempLabel: UILabel = { + let label = UILabel() + label.text = "--" + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + + private func setupUI() { + var customFont: UIFont + customFont = setupFont() + + topInfoStack.addArrangedSubview(minTempLabel) + topInfoStack.addArrangedSubview(currentTempLabel) + topInfoStack.addArrangedSubview(maxTempLabel) + + topStack.addArrangedSubview(currentCityLabel) + topStack.addArrangedSubview(topInfoStack) + + addSubview(topStack) + + // Really important +// minTempLabel.translatesAutoresizingMaskIntoConstraints = false +// currentTempLabel.translatesAutoresizingMaskIntoConstraints = false +// maxTempLabel.translatesAutoresizingMaskIntoConstraints = false +// currentCityLabel.translatesAutoresizingMaskIntoConstraints = false + topInfoStack.translatesAutoresizingMaskIntoConstraints = false + topStack.translatesAutoresizingMaskIntoConstraints = false + + // Set font for the labels + currentCityLabel.font = customFont + } + + func setupFont() -> UIFont { + guard let customFont = UIFont(name: "SpaceX", size: 24.0) else { + fatalError(""" + Failed to load the "SpaceX" font. + Make sure the font file is included in the project and the font name is spelled correctly. + """ + ) + } + return customFont + } + +} diff --git a/weatherkit-weather-app.xcodeproj/project.pbxproj b/weatherkit-weather-app.xcodeproj/project.pbxproj index 4f045b9..fafc520 100644 --- a/weatherkit-weather-app.xcodeproj/project.pbxproj +++ b/weatherkit-weather-app.xcodeproj/project.pbxproj @@ -8,9 +8,13 @@ /* Begin PBXBuildFile section */ C006DE2D2963CB5B00E8A44B /* InfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C006DE2C2963CB5B00E8A44B /* InfoVC.swift */; }; + C0160AB22B2A62A70016F9B0 /* SunsetSunriseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0160AB12B2A62A70016F9B0 /* SunsetSunriseView.swift */; }; + C0160AB32B2A62A70016F9B0 /* SunsetSunriseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0160AB12B2A62A70016F9B0 /* SunsetSunriseView.swift */; }; + C01634662D12422F00E63C45 /* MainScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01634652D12422F00E63C45 /* MainScrollView.swift */; }; + C01634672D12422F00E63C45 /* MainScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01634652D12422F00E63C45 /* MainScrollView.swift */; }; C034703F28F6221900656AA8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034703E28F6221900656AA8 /* AppDelegate.swift */; }; C034704128F6221900656AA8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034704028F6221900656AA8 /* SceneDelegate.swift */; }; - C034704328F6221900656AA8 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034704228F6221900656AA8 /* MainViewController.swift */; }; + C034704328F6221900656AA8 /* iPhoneMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034704228F6221900656AA8 /* iPhoneMainViewController.swift */; }; C034704828F6221A00656AA8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C034704728F6221A00656AA8 /* Assets.xcassets */; }; C034704B28F6221A00656AA8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C034704928F6221A00656AA8 /* LaunchScreen.storyboard */; }; C034706028FB08AF00656AA8 /* DateConverterUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034705F28FB08AF00656AA8 /* DateConverterUtil.swift */; }; @@ -57,6 +61,26 @@ C0EDB92D2A2429D300C5C1BF /* SpaceX.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C0EDB92C2A2429D300C5C1BF /* SpaceX.ttf */; }; C0EDB92E2A2429D300C5C1BF /* SpaceX.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C0EDB92C2A2429D300C5C1BF /* SpaceX.ttf */; }; C0F341142ABD2AA9006501A7 /* AppDelegateBackgrounRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F341132ABD2AA9006501A7 /* AppDelegateBackgrounRefresh.swift */; }; + C0FDAA522AF21E8A00BAB570 /* iPadMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA512AF21E8A00BAB570 /* iPadMainViewController.swift */; }; + C0FDAA532AF21E8A00BAB570 /* iPadMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA512AF21E8A00BAB570 /* iPadMainViewController.swift */; }; + C0FDAA562AF2C84E00BAB570 /* iPadMainTopCurrentStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA552AF2C84E00BAB570 /* iPadMainTopCurrentStack.swift */; }; + C0FDAA572AF2C84E00BAB570 /* iPadMainTopCurrentStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA552AF2C84E00BAB570 /* iPadMainTopCurrentStack.swift */; }; + C0FDAA5E2AF761FF00BAB570 /* LaunchRocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA5D2AF761FF00BAB570 /* LaunchRocket.swift */; }; + C0FDAA5F2AF761FF00BAB570 /* LaunchRocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA5D2AF761FF00BAB570 /* LaunchRocket.swift */; }; + C0FDAA622AF832C100BAB570 /* HumidityStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA612AF832C100BAB570 /* HumidityStackView.swift */; }; + C0FDAA632AF832C100BAB570 /* HumidityStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA612AF832C100BAB570 /* HumidityStackView.swift */; }; + C0FDAA6A2B0354FF00BAB570 /* UVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA692B0354FF00BAB570 /* UVView.swift */; }; + C0FDAA6E2B045CC900BAB570 /* PrecipitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA6D2B045CC900BAB570 /* PrecipitationView.swift */; }; + C0FDAA722B0478B400BAB570 /* WindView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA712B0478B400BAB570 /* WindView.swift */; }; + C0FDAA742B0478BD00BAB570 /* PrecipitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA6D2B045CC900BAB570 /* PrecipitationView.swift */; }; + C0FDAA752B0478BF00BAB570 /* UVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA692B0354FF00BAB570 /* UVView.swift */; }; + C0FDAA762B047CDB00BAB570 /* WindView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA712B0478B400BAB570 /* WindView.swift */; }; + C0FDAA782B04808200BAB570 /* PressureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA772B04808200BAB570 /* PressureView.swift */; }; + C0FDAA792B04808200BAB570 /* PressureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA772B04808200BAB570 /* PressureView.swift */; }; + C0FDAA7C2B0B311D00BAB570 /* iPadDailyCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA7B2B0B311D00BAB570 /* iPadDailyCollectionViewCell.swift */; }; + C0FDAA7D2B0B311D00BAB570 /* iPadDailyCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA7B2B0B311D00BAB570 /* iPadDailyCollectionViewCell.swift */; }; + C0FDAA802B0C340800BAB570 /* iPadWeatherKitCallUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA7F2B0C340800BAB570 /* iPadWeatherKitCallUtil.swift */; }; + C0FDAA812B0C340800BAB570 /* iPadWeatherKitCallUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FDAA7F2B0C340800BAB570 /* iPadWeatherKitCallUtil.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -92,15 +116,17 @@ /* Begin PBXFileReference section */ C006DE2C2963CB5B00E8A44B /* InfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoVC.swift; sourceTree = ""; }; + C0160AB12B2A62A70016F9B0 /* SunsetSunriseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunsetSunriseView.swift; sourceTree = ""; }; C01617CC298C6016006E7C0C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; C01617CE298C6018006E7C0C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C01617D1298C6018006E7C0C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; C01617DA298C6407006E7C0C /* weatherkit_watchosExtensionApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = weatherkit_watchosExtensionApp.swift; sourceTree = ""; }; + C01634652D12422F00E63C45 /* MainScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScrollView.swift; sourceTree = ""; }; C01B200D2995EC21000363CA /* CurrentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentView.swift; sourceTree = ""; }; C034703B28F6221900656AA8 /* weatherkit-weather-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "weatherkit-weather-app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; C034703E28F6221900656AA8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C034704028F6221900656AA8 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - C034704228F6221900656AA8 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + C034704228F6221900656AA8 /* iPhoneMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPhoneMainViewController.swift; sourceTree = ""; }; C034704728F6221A00656AA8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C034704A28F6221A00656AA8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; C034704C28F6221A00656AA8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -150,6 +176,16 @@ C0EDB9292A22BDF600C5C1BF /* WindSpeedPopUpVCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindSpeedPopUpVCTests.swift; sourceTree = ""; }; C0EDB92C2A2429D300C5C1BF /* SpaceX.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = SpaceX.ttf; sourceTree = ""; }; C0F341132ABD2AA9006501A7 /* AppDelegateBackgrounRefresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateBackgrounRefresh.swift; sourceTree = ""; }; + C0FDAA512AF21E8A00BAB570 /* iPadMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadMainViewController.swift; sourceTree = ""; }; + C0FDAA552AF2C84E00BAB570 /* iPadMainTopCurrentStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadMainTopCurrentStack.swift; sourceTree = ""; }; + C0FDAA5D2AF761FF00BAB570 /* LaunchRocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchRocket.swift; sourceTree = ""; }; + C0FDAA612AF832C100BAB570 /* HumidityStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HumidityStackView.swift; sourceTree = ""; }; + C0FDAA692B0354FF00BAB570 /* UVView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UVView.swift; sourceTree = ""; }; + C0FDAA6D2B045CC900BAB570 /* PrecipitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrecipitationView.swift; sourceTree = ""; }; + C0FDAA712B0478B400BAB570 /* WindView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindView.swift; sourceTree = ""; }; + C0FDAA772B04808200BAB570 /* PressureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PressureView.swift; sourceTree = ""; }; + C0FDAA7B2B0B311D00BAB570 /* iPadDailyCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadDailyCollectionViewCell.swift; sourceTree = ""; }; + C0FDAA7F2B0C340800BAB570 /* iPadWeatherKitCallUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadWeatherKitCallUtil.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -247,6 +283,7 @@ C034703D28F6221900656AA8 /* programmatic-ui-weather-app */ = { isa = PBXGroup; children = ( + C0FDAA502AF21E0B00BAB570 /* iPad Version */, C0B01BCA2912B08A00D887C6 /* programmatic-ui-weather-app.entitlements */, C034705328F640AD00656AA8 /* Delegates */, C0A211182952E20B00C05917 /* AnimationsEffects */, @@ -373,7 +410,7 @@ isa = PBXGroup; children = ( C072FF442A742F6700CCF9B1 /* HourlyCollectionViewCell */, - C034704228F6221900656AA8 /* MainViewController.swift */, + C034704228F6221900656AA8 /* iPhoneMainViewController.swift */, ); path = MainVC; sourceTree = ""; @@ -452,6 +489,81 @@ path = Fonts; sourceTree = ""; }; + C0FDAA502AF21E0B00BAB570 /* iPad Version */ = { + isa = PBXGroup; + children = ( + C0FDAA652AFB48DE00BAB570 /* iPad View Controllers */, + C0FDAA5C2AF761E100BAB570 /* Rocket Utils */, + C0FDAA542AF2C82600BAB570 /* iPadMainTopCurrentStack */, + ); + path = "iPad Version"; + sourceTree = ""; + }; + C0FDAA542AF2C82600BAB570 /* iPadMainTopCurrentStack */ = { + isa = PBXGroup; + children = ( + C0FDAA552AF2C84E00BAB570 /* iPadMainTopCurrentStack.swift */, + ); + path = iPadMainTopCurrentStack; + sourceTree = ""; + }; + C0FDAA5C2AF761E100BAB570 /* Rocket Utils */ = { + isa = PBXGroup; + children = ( + C0FDAA5D2AF761FF00BAB570 /* LaunchRocket.swift */, + ); + path = "Rocket Utils"; + sourceTree = ""; + }; + C0FDAA652AFB48DE00BAB570 /* iPad View Controllers */ = { + isa = PBXGroup; + children = ( + C0FDAA7E2B0C33F000BAB570 /* Utils */, + C0FDAA6B2B03560500BAB570 /* MainViewController */, + ); + path = "iPad View Controllers"; + sourceTree = ""; + }; + C0FDAA6B2B03560500BAB570 /* MainViewController */ = { + isa = PBXGroup; + children = ( + C0FDAA7A2B0B30F900BAB570 /* iPadDailyCollectionViewCell */, + C0FDAA6C2B03561300BAB570 /* Components */, + C0FDAA512AF21E8A00BAB570 /* iPadMainViewController.swift */, + ); + path = MainViewController; + sourceTree = ""; + }; + C0FDAA6C2B03561300BAB570 /* Components */ = { + isa = PBXGroup; + children = ( + C0FDAA692B0354FF00BAB570 /* UVView.swift */, + C0FDAA6D2B045CC900BAB570 /* PrecipitationView.swift */, + C0FDAA712B0478B400BAB570 /* WindView.swift */, + C0FDAA772B04808200BAB570 /* PressureView.swift */, + C0160AB12B2A62A70016F9B0 /* SunsetSunriseView.swift */, + C0FDAA612AF832C100BAB570 /* HumidityStackView.swift */, + C01634652D12422F00E63C45 /* MainScrollView.swift */, + ); + path = Components; + sourceTree = ""; + }; + C0FDAA7A2B0B30F900BAB570 /* iPadDailyCollectionViewCell */ = { + isa = PBXGroup; + children = ( + C0FDAA7B2B0B311D00BAB570 /* iPadDailyCollectionViewCell.swift */, + ); + path = iPadDailyCollectionViewCell; + sourceTree = ""; + }; + C0FDAA7E2B0C33F000BAB570 /* Utils */ = { + isa = PBXGroup; + children = ( + C0FDAA7F2B0C340800BAB570 /* iPadWeatherKitCallUtil.swift */, + ); + path = Utils; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -605,26 +717,38 @@ C0CF2DD02929487800359476 /* SettingsCell.swift in Sources */, C0A2111A2952E21C00C05917 /* AnimationsEffects.swift in Sources */, C06F8D31292981680054D5DC /* Forecasts.swift in Sources */, + C0FDAA5E2AF761FF00BAB570 /* LaunchRocket.swift in Sources */, C006DE2D2963CB5B00E8A44B /* InfoVC.swift in Sources */, C0F341142ABD2AA9006501A7 /* AppDelegateBackgrounRefresh.swift in Sources */, + C0FDAA6A2B0354FF00BAB570 /* UVView.swift in Sources */, C034706028FB08AF00656AA8 /* DateConverterUtil.swift in Sources */, C0711C2C2949667B004D3BD3 /* WindSpeedPopUpVC.swift in Sources */, C0B01BF3291F361900D887C6 /* TabBarViewController.swift in Sources */, C0711C2929484FD3004D3BD3 /* SunriseSunsetPopUpVC.swift in Sources */, C0D265C0297635480079FBE3 /* WeatherKitData.swift in Sources */, C04E9E9F29417FA100882FC6 /* WidgetStruct.swift in Sources */, + C0FDAA562AF2C84E00BAB570 /* iPadMainTopCurrentStack.swift in Sources */, C072FF462A742F8900CCF9B1 /* HourlyCollectionViewCell.swift in Sources */, C0CF2DD22929487800359476 /* SettingsListVC.swift in Sources */, - C034704328F6221900656AA8 /* MainViewController.swift in Sources */, + C034704328F6221900656AA8 /* iPhoneMainViewController.swift in Sources */, + C0FDAA622AF832C100BAB570 /* HumidityStackView.swift in Sources */, C05606FE2A61CEBA00F3C705 /* SwiftUIBackgroundBlur.swift in Sources */, C0CF2DD12929487800359476 /* Settings.swift in Sources */, + C0FDAA782B04808200BAB570 /* PressureView.swift in Sources */, C04E9EEE2945296C00882FC6 /* PrimaryData.swift in Sources */, + C0FDAA722B0478B400BAB570 /* WindView.swift in Sources */, C06F8D2E292980550054D5DC /* ForecastListVC.swift in Sources */, + C0160AB22B2A62A70016F9B0 /* SunsetSunriseView.swift in Sources */, + C0FDAA802B0C340800BAB570 /* iPadWeatherKitCallUtil.swift in Sources */, C055633C295CE4CB008CB6D4 /* NotificationUtil.swift in Sources */, C05606FC2A612DA800F3C705 /* PrecipitationPopUpVC.swift in Sources */, + C01634662D12422F00E63C45 /* MainScrollView.swift in Sources */, C0B01BCC291334AC00D887C6 /* WeatherKitCallUtil.swift in Sources */, C034703F28F6221900656AA8 /* AppDelegate.swift in Sources */, + C0FDAA522AF21E8A00BAB570 /* iPadMainViewController.swift in Sources */, + C0FDAA7C2B0B311D00BAB570 /* iPadDailyCollectionViewCell.swift in Sources */, C034704128F6221900656AA8 /* SceneDelegate.swift in Sources */, + C0FDAA6E2B045CC900BAB570 /* PrecipitationView.swift in Sources */, C053221E295D52F100FF849E /* RocketAnimationUtil.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -643,8 +767,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C0FDAA742B0478BD00BAB570 /* PrecipitationView.swift in Sources */, + C0FDAA7D2B0B311D00BAB570 /* iPadDailyCollectionViewCell.swift in Sources */, C0EDB9232A22BDD400C5C1BF /* weatherkit_weather_apptests.swift in Sources */, + C0FDAA5F2AF761FF00BAB570 /* LaunchRocket.swift in Sources */, + C01634672D12422F00E63C45 /* MainScrollView.swift in Sources */, + C0FDAA762B047CDB00BAB570 /* WindView.swift in Sources */, C0EDB92A2A22BDF600C5C1BF /* WindSpeedPopUpVCTests.swift in Sources */, + C0FDAA752B0478BF00BAB570 /* UVView.swift in Sources */, + C0FDAA532AF21E8A00BAB570 /* iPadMainViewController.swift in Sources */, + C0FDAA792B04808200BAB570 /* PressureView.swift in Sources */, + C0FDAA572AF2C84E00BAB570 /* iPadMainTopCurrentStack.swift in Sources */, + C0160AB32B2A62A70016F9B0 /* SunsetSunriseView.swift in Sources */, + C0FDAA632AF832C100BAB570 /* HumidityStackView.swift in Sources */, + C0FDAA812B0C340800BAB570 /* iPadWeatherKitCallUtil.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -707,6 +843,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -767,6 +904,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; @@ -797,7 +935,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "programmatic-ui-weather-app/programmatic-ui-weather-app.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = PT5J4Z6J5K; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "programmatic-ui-weather-app/Info.plist"; @@ -816,7 +954,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.6.6; + MARKETING_VERSION = 0.9.1; PRODUCT_BUNDLE_IDENTIFIER = "com.ES.weatherkit-programmatic-app"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -824,7 +962,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -836,7 +974,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "programmatic-ui-weather-app/programmatic-ui-weather-app.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = PT5J4Z6J5K; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "programmatic-ui-weather-app/Info.plist"; @@ -855,7 +993,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.6.6; + MARKETING_VERSION = 0.9.1; PRODUCT_BUNDLE_IDENTIFIER = "com.ES.weatherkit-programmatic-app"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -863,7 +1001,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; diff --git a/weatherkit-weather-app.xcodeproj/project.xcworkspace/xcuserdata/RuslanS.xcuserdatad/UserInterfaceState.xcuserstate b/weatherkit-weather-app.xcodeproj/project.xcworkspace/xcuserdata/RuslanS.xcuserdatad/UserInterfaceState.xcuserstate index 39cfb4f..d42b9ff 100644 Binary files a/weatherkit-weather-app.xcodeproj/project.xcworkspace/xcuserdata/RuslanS.xcuserdatad/UserInterfaceState.xcuserstate and b/weatherkit-weather-app.xcodeproj/project.xcworkspace/xcuserdata/RuslanS.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/weatherkit-weather-app.xcodeproj/xcuserdata/RuslanS.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/weatherkit-weather-app.xcodeproj/xcuserdata/RuslanS.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 8f36f14..948530b 100644 --- a/weatherkit-weather-app.xcodeproj/xcuserdata/RuslanS.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/weatherkit-weather-app.xcodeproj/xcuserdata/RuslanS.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -176,8 +176,8 @@ filePath = "programmatic-ui-weather-app/Utils/WeatherKitCallUtil.swift" startingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807" - startingLineNumber = "126" - endingLineNumber = "126" + startingLineNumber = "127" + endingLineNumber = "127" landmarkName = "getWeather(location:)" landmarkType = "7"> @@ -224,8 +224,8 @@ filePath = "programmatic-ui-weather-app/Utils/WeatherKitCallUtil.swift" startingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807" - startingLineNumber = "147" - endingLineNumber = "147" + startingLineNumber = "149" + endingLineNumber = "149" landmarkName = "getWeather(location:)" landmarkType = "7"> @@ -258,9 +258,41 @@ endingColumnNumber = "9223372036854775807" startingLineNumber = "19" endingLineNumber = "19" - landmarkName = "MainViewController" + landmarkName = "iPhoneMainViewController" landmarkType = "3"> + + + + + + + +