Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 5 & 6 #22

Merged
merged 26 commits into from
Jun 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e721aac
Initial TDD for DatabaseService
devahmedshendy Apr 30, 2023
8d8d6d3
Add GRDB dependency using project.yml instead
devahmedshendy Apr 30, 2023
b2d0189
Write test for inserting favorite location
devahmedshendy Apr 30, 2023
21ec3e6
Remove constants of sqlColumn/sqlTable
devahmedshendy Apr 30, 2023
77009ef
Fix: uuid shouldn't be saved, but is string representation
devahmedshendy Apr 30, 2023
9515f49
Clean up
devahmedshendy Apr 30, 2023
e98c6c1
Refactor sql statements to find location by name, lat, long instead
devahmedshendy May 3, 2023
9674263
Use LocationProvider, and make view models as StateObject
devahmedshendy May 3, 2023
5e45f7a
Add created_at to fetch favorites as sorted
devahmedshendy May 3, 2023
6b89edd
Change some 'summary' namings to 'favorites'
devahmedshendy May 3, 2023
7ef1412
Implement Feature 5 & 6
devahmedshendy May 16, 2023
08d2f23
DatabaseError, memory leak, cleanup
devahmedshendy May 17, 2023
4de9a0c
Apple fixes
devahmedshendy May 18, 2023
35c8445
Rename database to databaseService
devahmedshendy May 18, 2023
147f515
refactor LocationAdapterTest
devahmedshendy May 20, 2023
491ce4b
Mentioned ticket #23
devahmedshendy May 20, 2023
fc37985
rewrite/add-more tests for SQLiteDatabaseServiceTests
devahmedshendy May 26, 2023
301e586
Write unit test for toggleLocation of SearchLocationViewModel
devahmedshendy May 26, 2023
be30d6b
Divide unit test for toggleFavorite in SearchLocationViewModel to sea…
devahmedshendy May 26, 2023
07fc7d6
Add tests for FavoritesViewModel
devahmedshendy May 26, 2023
a857cbc
Add test for weather summary for FavoritesViewModel
devahmedshendy May 28, 2023
fe86f10
Use tagged version for GRDB than master branch
devahmedshendy Jun 1, 2023
a71a787
Resolve conversation
devahmedshendy Jun 1, 2023
58a025e
Fix project.yml syntax, 'from' instead of 'branch'
devahmedshendy Jun 1, 2023
148f647
Sync unfavorites with search result
devahmedshendy Jun 7, 2023
726c93a
clean up
devahmedshendy Jun 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions OpenWeather/App/Favorites/FavoritesScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// OpenWeather template generated by OpenBytes on 16/03/2023.
//
// Created by Ahmed Shendy.
// FavoritesScreen.swift
//

import SwiftUI
import CoreLocation

struct FavoritesScreen: View {
@StateObject var viewModel: FavoritesViewModel

var body: some View {
viewModel.view { content in
if content.summaries.isEmpty {
Text("No Favorites Available")
} else {
List {
ForEach(content.summaries) { favorite in
LocationWeatherItem(
locationName: favorite.locationName,
temperature: favorite.temperature,
symbolName: favorite.symbolName
)
.swipeActions(allowsFullSwipe: false) {
Button(role: .destructive) {
viewModel.removeFavoriteLocation(favorite.location)
} label: {
Label("Remove", systemImage: "heart.slash")
}
.tint(.red)
}
}
}
}
}
}
}

struct LocationWeatherItem: View {
let locationName: String
let temperature: String
let symbolName: String

var body: some View {
HStack {
Text(locationName)
Spacer()
Text(temperature)
Image(systemName: symbolName)
}
}
}

struct SummaryScreen_Previews: PreviewProvider {
static var previews: some View {
FavoritesScreen(viewModel: .mock)
}
}
135 changes: 135 additions & 0 deletions OpenWeather/App/Favorites/FavoritesViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//
// OpenWeather template generated by OpenBytes on 16/03/2023.
//
// Created by Ahmed Shendy.
// FavoritesViewModel.swift
//

import Combine
import ViewModel
import Foundation
import CoreLocation

final class FavoritesViewModel: ViewModel<
FavoritesViewModel.Capabilities,
FavoritesViewModel.Input,
FavoritesViewModel.Content
> {
struct Capabilities {
static var mock: Capabilities {
.init(
locationProviding: MockLocationProvider(),
weatherProviding: MockWeatherProvider(),
databaseService: MockDatabaseService()
)
}

private let weatherProviding: WeatherProviding
private let locationProviding: LocationProviding
private let databaseService: DatabaseService

init(
locationProviding: LocationProviding,
weatherProviding: WeatherProviding,
databaseService: DatabaseService
) {
self.locationProviding = locationProviding
self.weatherProviding = weatherProviding
self.databaseService = databaseService
}

func locationName(for location: CLLocation) async throws -> String {
try await locationProviding.locationName(for: location)
}

func weather(for location: CLLocation) async throws -> DeviceWeather {
try await weatherProviding.weather(for: location)
}

func getFavoritesPublisher() -> AnyPublisher<[DeviceLocation], Error> {
databaseService.fetchAllFavoritesPublisher()
}

func removeFavorite(_ location: DeviceLocation) async throws {
try await databaseService.deleteOneFavorite(location)
}
}

struct Input { }

struct Content {
let summaries: [DeviceWeatherSummary]
}

override var content: Content {
Content(summaries: summaries)
}

static var mock: FavoritesViewModel {
.init(capabilities: .mock, input: .init())
}

private let errorHandler: ErrorHandler
private var subscription: AnyCancellable?

@Published private var summaries: [DeviceWeatherSummary] = []

init(
errorHandler: ErrorHandler = ErrorHandler(
plugins: [ToastErrorPlugin()]
),
capabilities: Capabilities,
input: Input
) {
self.errorHandler = errorHandler

super.init(capabilities: capabilities, input: input)

observeChangesInFavorites()
}

private func observeChangesInFavorites() {
subscription = self.capabilities
devahmedshendy marked this conversation as resolved.
Show resolved Hide resolved
.getFavoritesPublisher()
.flatMap { $0
.publisher
.setFailureType(to: Error.self)
.asyncCompactMap(self.getWeatherSummary(for:))
devahmedshendy marked this conversation as resolved.
Show resolved Hide resolved
.collect()
}
.receive(on: RunLoop.main)
.sink(
receiveCompletion: { [weak self] completion in
if case let .failure(error) = completion {
self?.errorHandler.handle(error: error)
}
},
receiveValue: { [weak self] summaries in
self?.summaries = summaries
}
)
}

private func getWeatherSummary(
for location: DeviceLocation
) async throws -> DeviceWeatherSummary? {
guard
let weather = try? await self.capabilities.weather(for: location.location)
else { return nil }

return DeviceWeatherSummary(
location: location,
weather: weather
)
}

func removeFavoriteLocation(_ location: DeviceLocation) {
Task {
do {
try await capabilities.removeFavorite(location)
} catch {
errorHandler.handle(error: error)
}
}
}
}
6 changes: 1 addition & 5 deletions OpenWeather/App/Home/HomeScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ struct HomeScreen: View {
private let location: CLLocation = Mock.londonLocation

@ObservedObject var settings: AppSettings = AppSettings.shared
@ObservedObject var viewModel: HomeViewModel

init(viewModel: HomeViewModel) {
self.viewModel = viewModel
}
@StateObject var viewModel: HomeViewModel

var body: some View {
viewModel.view { content in
Expand Down
4 changes: 2 additions & 2 deletions OpenWeather/App/Home/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ final class HomeViewModel: ViewModel<
)
}

private var locationProviding: LocationProviding
private var weatherProviding: WeatherProviding
private let locationProviding: LocationProviding
private let weatherProviding: WeatherProviding

init(locationProviding: LocationProviding, weatherProviding: WeatherProviding) {
self.locationProviding = locationProviding
Expand Down
2 changes: 1 addition & 1 deletion OpenWeather/App/OpenWeatherApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import SwiftUI

@main
struct OBOpenWeatherApp: App {
@ObservedObject var navigation: Navigation = Navigation.shared
@StateObject var navigation: Navigation = Navigation.shared

var body: some Scene {
WindowGroup {
Expand Down
38 changes: 26 additions & 12 deletions OpenWeather/App/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct RootView: View {
enum Tab {
case home
case search
case summary
case favorites
case profile
}

Expand All @@ -30,11 +30,10 @@ struct RootView: View {
var body: some View {
TabView(selection: $navigation.tab) {
OpenBytesNavigationView(path: navigation.home) {
// TODO: Update to Production
HomeScreen(
viewModel: HomeViewModel(
capabilities: .init(
locationProviding: MockLocationProvider(),
locationProviding: LocationProvider(),
weatherProviding: OpenWeatherMapWeatherProvider()
),
input: .init()
Expand All @@ -48,23 +47,38 @@ struct RootView: View {
}

OpenBytesNavigationView(path: navigation.search) {
// TODO: Update to Production
SearchLocationScreen(viewModel: .mock)
SearchLocationScreen(
viewModel: .init(
capabilities: .init(
locationProviding: LocationProvider(),
databaseService: SQLiteDatabaseService.shared
),
input: .init()
)
)
}
.tag(Tab.search)
.tabItem {
Image(systemName: "magnifyingglass")
Image(systemName: "location.magnifyingglass")
Text("Search")
}

OpenBytesNavigationView(path: navigation.summary) {
// TODO: Update to Production
SummaryScreen(viewModel: .mock)
OpenBytesNavigationView(path: navigation.favorites) {
FavoritesScreen(
viewModel: .init(
capabilities: .init(
locationProviding: LocationProvider(),
weatherProviding: OpenWeatherMapWeatherProvider(),
databaseService: SQLiteDatabaseService.shared
),
input: .init()
)
)
}
.tag(Tab.summary)
.tag(Tab.favorites)
.tabItem {
Image(systemName: "cloud.fog.circle")
Text("Summary")
Image(systemName: "heart")
Text("Favorites")
}

OpenBytesNavigationView(path: navigation.profile) {
Expand Down
8 changes: 8 additions & 0 deletions OpenWeather/App/RootViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//
// OpenWeather template generated by OpenBytes on 30/04/2023.
//
// Created by Ahmed Shendy.
// RootViewModel.swift
//

import Foundation
11 changes: 5 additions & 6 deletions OpenWeather/App/Search/SearchLocationScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,18 @@ import OpenBytesNavigation
import SwiftUI

struct SearchLocationScreen: View {
@ObservedObject var viewModel: SearchLocationViewModel

init(viewModel: SearchLocationViewModel) {
self.viewModel = viewModel
}
@StateObject var viewModel: SearchLocationViewModel

var body: some View {
viewModel.view { content in
List {
ForEach(content.result) { location in
LocationResultItem(name: location.name)
LocationResultItem(location: location) {
self.viewModel.toggleFavorite(for: location)
}
devahmedshendy marked this conversation as resolved.
Show resolved Hide resolved
}
}
.buttonStyle(.borderless)
.searchable(
text: viewModel.binding(\.searchText),
prompt: "Search by name or zipcode"
Expand Down
Loading