Skip to content

Commit

Permalink
Create current temperature field
Browse files Browse the repository at this point in the history
Bump version number
Clean up code
  • Loading branch information
pawelmilek committed Mar 28, 2024
1 parent 391f7f7 commit c905e67
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 42 deletions.
14 changes: 10 additions & 4 deletions SwiftyForecast.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@
E2D430F22BB4DC1C003F26EE /* HourlyForecastChartDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D430AB2BB4D954003F26EE /* HourlyForecastChartDataSource.swift */; };
E2D430F32BB4DC24003F26EE /* BackgroundGroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D430AD2BB4D954003F26EE /* BackgroundGroupBoxStyle.swift */; };
E2D430F42BB4DC30003F26EE /* MotionAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E224506F2B9F3F9800F14BAA /* MotionAnimationView.swift */; };
E2D430F62BB4EBB9003F26EE /* MKCoordinateRegion+Equtable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D430F52BB4EBB9003F26EE /* MKCoordinateRegion+Equtable.swift */; };
E2D430F72BB4EBB9003F26EE /* MKCoordinateRegion+Equtable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D430F52BB4EBB9003F26EE /* MKCoordinateRegion+Equtable.swift */; };
E2D960812120F34800AE0B6E /* Geocoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D960802120F34800AE0B6E /* Geocoder.swift */; };
E2DC1458248D2F8700D1A329 /* InvalidReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DC1457248D2F8700D1A329 /* InvalidReference.swift */; };
E2DC1466248D493700D1A329 /* JSONFileLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DC1465248D493700D1A329 /* JSONFileLoader.swift */; };
Expand Down Expand Up @@ -463,6 +465,7 @@
E2D430B82BB4D954003F26EE /* LocationWeatherView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationWeatherView.swift; sourceTree = "<group>"; };
E2D430B92BB4D954003F26EE /* LocationWeatherViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationWeatherViewViewModel.swift; sourceTree = "<group>"; };
E2D430BB2BB4D954003F26EE /* LocationWeatherViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationWeatherViewModel.swift; sourceTree = "<group>"; };
E2D430F52BB4EBB9003F26EE /* MKCoordinateRegion+Equtable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MKCoordinateRegion+Equtable.swift"; sourceTree = "<group>"; };
E2D960802120F34800AE0B6E /* Geocoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Geocoder.swift; sourceTree = "<group>"; };
E2DC1457248D2F8700D1A329 /* InvalidReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvalidReference.swift; sourceTree = "<group>"; };
E2DC1465248D493700D1A329 /* JSONFileLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONFileLoader.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -712,6 +715,7 @@
E2DC1474248F221E00D1A329 /* UIViewController+Containment.swift */,
E2DC148A248F24E800D1A329 /* UIViewController+MakeFromStoryboard.swift */,
E2B1D655211E55F000F0002A /* UIViewController+StoryboardIdentifiable.swift */,
E2D430F52BB4EBB9003F26EE /* MKCoordinateRegion+Equtable.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -1612,6 +1616,7 @@
E290F24D2ADD8BDE001B3276 /* SpeedFormatterMetric.swift in Sources */,
E2DC1458248D2F8700D1A329 /* InvalidReference.swift in Sources */,
E28CEC0F2B3EB775003DB3A6 /* LocationsTip.swift in Sources */,
E2D430F62BB4EBB9003F26EE /* MKCoordinateRegion+Equtable.swift in Sources */,
E2D430BF2BB4D954003F26EE /* BackgroundGroupBoxStyle.swift in Sources */,
E270891C24CE5AEC003F7587 /* Coordinator.swift in Sources */,
E2A5675D2B077AC9000324C0 /* LottieAnimationViewController.swift in Sources */,
Expand Down Expand Up @@ -1660,6 +1665,7 @@
E2AD326A2B23774400F46FBE /* URLRequest+Parameters.swift in Sources */,
E2CFA7AB2B309E63001DE3E8 /* MockModelGenerator.swift in Sources */,
E2D430E82BB4DB94003F26EE /* SpeedFormatterMetric.swift in Sources */,
E2D430F72BB4EBB9003F26EE /* MKCoordinateRegion+Equtable.swift in Sources */,
E2D430E72BB4DB89003F26EE /* SpeedValueDisplayable.swift in Sources */,
E2F671232B23682F00B791A0 /* WeatherProvider.swift in Sources */,
E2D430EE2BB4DC04003F26EE /* PathFinder.swift in Sources */,
Expand Down Expand Up @@ -1901,7 +1907,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.5.7;
MARKETING_VERSION = 3.6.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.pawelmilek.Swifty-Forecast";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -1935,7 +1941,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.5.7;
MARKETING_VERSION = 3.6.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.pawelmilek.Swifty-Forecast";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -2045,7 +2051,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 3.5.7;
MARKETING_VERSION = 3.6.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.pawelmilek.Swifty-Forecast.widget";
Expand Down Expand Up @@ -2092,7 +2098,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 3.5.7;
MARKETING_VERSION = 3.6.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.pawelmilek.Swifty-Forecast.widget";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ struct LocationList: View {
.tint(Color(.customPrimary))
.listRowSeparator(.hidden)
ForEach(locations) { location in
LocationRow(item: location)
LocationRow(viewModel: LocationRowViewModel(
location: location,
service: WeatherService(),
temperatureRenderer: TemperatureRenderer(),
measurementSystemNotification: MeasurementSystemNotification()
))
.deleteDisabled(location.isUserLocation)
.onTapGesture {
onSelectRow(location)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,116 @@

import SwiftUI
import MapKit
import Combine

@MainActor
final class LocationRowViewModel: ObservableObject {
@Published private(set) var error: Error?
@Published private(set) var isLoading = false
@Published private(set) var locationName = ""
@Published private(set) var temperature = ""
@Published private(set) var localTime = ""
@Published private(set) var name = ""
@Published private(set) var region = MKCoordinateRegion()
@Published private(set) var temperatureValue: TemperatureValue?
@Published private(set) var location: LocationModel?

private let service: WeatherServiceProtocol
private let measurementSystemNotification: MeasurementSystemNotification
private let temperatureRenderer: TemperatureRenderer
private var cancellables = Set<AnyCancellable>()

init(
location: LocationModel,
service: WeatherServiceProtocol,
temperatureRenderer: TemperatureRenderer,
measurementSystemNotification: MeasurementSystemNotification) {
self.service = service
self.temperatureRenderer = temperatureRenderer
self.measurementSystemNotification = measurementSystemNotification

subscribeToPublisher()
registerMeasurementSystemObserver()
self.location = location
}

func loadData(at locationModel: LocationModel) {
guard !isLoading else { return }
isLoading = true

locationName = locationModel.name
let latitude = locationModel.latitude
let longitude = locationModel.longitude

Task(priority: .userInitiated) {
do {
let currentResponse = try await service.fetchCurrent(
latitude: latitude,
longitude: longitude
)
let model = ResponseParser.parse(current: currentResponse)
temperatureValue = model.temperatureValue
isLoading = false
} catch {
self.error = error
temperatureValue = nil
isLoading = false
}
}
}

private func subscribeToPublisher() {
$location
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink { [self] location in
localTime = Date.timeOnly(from: location.secondsFromGMT)
name = location.name + ", " + location.country

let annotation = MKPointAnnotation()
annotation.subtitle = "\(location.name) \(location.state)"
annotation.coordinate = CLLocationCoordinate2D(
latitude: location.latitude,
longitude: location.longitude
)

let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
region = MKCoordinateRegion(center: annotation.coordinate, span: span)
loadData(at: location)
}
.store(in: &cancellables)

$temperatureValue
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink { [self] temperatureValue in
setTemperatureAccordingToUnitNotation(value: temperatureValue)
}
.store(in: &cancellables)
}

private func registerMeasurementSystemObserver() {
measurementSystemNotification.addObserver(
self,
selector: #selector(measurementSystemChanged)
)
}

@objc
private func measurementSystemChanged() {
guard let temperatureValue else { return }
setTemperatureAccordingToUnitNotation(value: temperatureValue)
}

private func setTemperatureAccordingToUnitNotation(value: TemperatureValue) {
let rendered = temperatureRenderer.render(value)
temperature = rendered.currentFormatted
}
}

struct LocationRow: View {
let item: LocationModel
@State private var position: MapCameraPosition = .automatic
@ObservedObject var viewModel: LocationRowViewModel

var body: some View {
VStack(alignment: .leading, spacing: 5) {
Expand All @@ -35,55 +141,51 @@ struct LocationRow: View {
.frame(minHeight: 145, maxHeight: 145)
.fixedSize(horizontal: false, vertical: true)
.listRowInsets(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12))
.onAppear {
position = .region(region)
.onChange(of: viewModel.region, initial: false) {
position = .region(viewModel.region)
}
}
}

private extension LocationRow {

var headerView: some View {
VStack(alignment: .leading, spacing: 0) {
Text(localTime)
.font(Style.LocationRow.timeFont)
.foregroundStyle(.customPrimary)
Text(name)
.font(Style.LocationRow.nameFont)
.foregroundStyle(Style.LocationRow.nameColor)
HStack {
VStack(alignment: .leading, spacing: 0) {
Text(viewModel.localTime)
.font(Style.LocationRow.timeFont)
.foregroundStyle(.customPrimary)
Text(viewModel.name)
.font(Style.LocationRow.nameFont)
.foregroundStyle(Style.LocationRow.nameColor)
}
.frame(maxWidth: .infinity, alignment: .leading)
Text(viewModel.temperature)
.font(Style.LocationRow.tempFont)
.foregroundStyle(Style.LocationRow.tempColor)
.overlay {
ProgressView()
.tint(.customPrimary)
.opacity(viewModel.isLoading ? 1 : 0)
.animation(.easeOut, value: viewModel.isLoading)
}
}
}

var mapView: some View {
Map(position: $position, interactionModes: []) {
Marker(name, coordinate: region.center)
Marker(viewModel.name, coordinate: viewModel.region.center)
.tint(.customPrimary)
}
.cornerRadius(Style.LocationRow.cornerRadius)
}

var localTime: String {
Date.timeOnly(from: item.secondsFromGMT)
}

var name: String {
item.name + ", " + item.country
}

var region: MKCoordinateRegion {
let annotation = MKPointAnnotation()
annotation.subtitle = "\(item.name) \(item.state)"
annotation.coordinate = CLLocationCoordinate2D(
latitude: item.latitude,
longitude: item.longitude
)

let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let region = MKCoordinateRegion(center: annotation.coordinate, span: span)
return region
}
}

#Preview(traits: .sizeThatFitsLayout) {
LocationRow(item: LocationModel.examples.first!)
LocationRow(viewModel: LocationRowViewModel(
location: LocationModel.examples.first!,
service: WeatherService(),
temperatureRenderer: TemperatureRenderer(),
measurementSystemNotification: MeasurementSystemNotification()
))
}
16 changes: 16 additions & 0 deletions SwiftyForecast/Shared/Extensions/MKCoordinateRegion+Equtable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// MKCoordinateRegion+Equtable.swift
// SwiftyForecast
//
// Created by Pawel Milek on 3/27/24.
// Copyright © 2024 Pawel Milek. All rights reserved.
//

import MapKit

extension MKCoordinateRegion: Equatable {
public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool {
(lhs.center.latitude == rhs.center.latitude && lhs.center.longitude == rhs.center.longitude) &&
(lhs.span.latitudeDelta == rhs.span.latitudeDelta && lhs.span.longitudeDelta == rhs.span.longitudeDelta)
}
}
9 changes: 5 additions & 4 deletions SwiftyForecast/Shared/ThemeStyle/Style.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,13 @@ struct Style {
// MARK: - LocationRow
struct LocationRow {
static let backgroundColor = UIColor.clear
static let timeFont = Font.system(.subheadline, design: .monospaced, weight: .semibold)
static let timeAlignment = NSTextAlignment.left
static let timeFont = Font.system(.subheadline, design: .monospaced, weight: .bold)

static let nameFont = Font.system(.subheadline, design: .monospaced, weight: .semibold)
static let nameFont = Font.system(.subheadline, design: .monospaced, weight: .bold)
static let nameColor = Color(.accent)
static let locationNameAlignment = NSTextAlignment.left

static let tempFont = Font.system(.title2, design: .monospaced, weight: .heavy)
static let tempColor = Color(.customPrimary)

static let cornerRadius = CGFloat(15)
static let borderColor = Color(.shadow)
Expand Down

0 comments on commit c905e67

Please sign in to comment.