@shaykec/app-agent 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/AppConfig.kt +9 -0
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/data/DataSourceResolver.kt +23 -0
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/features/booking/BookingScreen.kt +1 -1
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/features/discovery/ProviderDetailScreen.kt +1 -1
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/features/discovery/SearchScreen.kt +1 -1
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/mock/MockDataProvider.kt +12 -11
- package/templates/android/MapTemplate/app/src/main/kotlin/com/appship/map/AppConfig.kt +9 -0
- package/templates/android/MapTemplate/app/src/main/kotlin/com/appship/map/data/DataRepository.kt +33 -0
- package/templates/android/MapTemplate/app/src/main/kotlin/com/appship/map/data/DataSourceResolver.kt +23 -0
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/AppConfig.kt +9 -0
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/data/DataRepository.kt +62 -0
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/data/DataSourceResolver.kt +23 -0
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/mock/MockDataProvider.kt +28 -28
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/AppConfig.kt +9 -0
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/data/DataRepository.kt +30 -0
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/data/DataSourceResolver.kt +23 -0
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/create/CreateScreen.kt +3 -2
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/detail/DetailScreen.kt +4 -3
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/explore/ExploreScreen.kt +7 -6
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/favorites/FavoritesScreen.kt +4 -3
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/home/HomeScreen.kt +5 -4
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/mock/MockDataProvider.kt +12 -11
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/AppConfig.kt +9 -0
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/data/DataRepository.kt +38 -0
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/data/DataSourceResolver.kt +23 -0
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/features/feed/FeedScreen.kt +2 -2
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/features/profile/ProfileScreen.kt +1 -1
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/features/search/SearchScreen.kt +1 -1
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/mock/MockDataProvider.kt +7 -6
- package/templates/ios/BookTemplate/BookTemplate/App/AppConfig.swift +12 -2
- package/templates/ios/BookTemplate/BookTemplate/Data/DataRepository.swift +33 -0
- package/templates/ios/BookTemplate/BookTemplate/Data/DataSourceResolver.swift +20 -0
- package/templates/ios/BookTemplate/BookTemplate/Features/Booking/BookingView.swift +1 -1
- package/templates/ios/BookTemplate/BookTemplate/Features/Discovery/DiscoveryView.swift +3 -3
- package/templates/ios/BookTemplate/BookTemplate/Features/Discovery/MapExploreView.swift +1 -1
- package/templates/ios/BookTemplate/BookTemplate/Features/Discovery/ProviderDetailView.swift +1 -1
- package/templates/ios/BookTemplate/BookTemplate/Features/Discovery/SearchView.swift +2 -2
- package/templates/ios/BookTemplate/BookTemplate/MockData/MockDataProvider.swift +18 -1
- package/templates/ios/MapTemplate/MapTemplate/App/AppConfig.swift +11 -9
- package/templates/ios/MapTemplate/MapTemplate/Data/DataSourceResolver.swift +20 -0
- package/templates/ios/MapTemplate/MapTemplate/Features/Favorites/FavoritesView.swift +1 -1
- package/templates/ios/MapTemplate/MapTemplate/Features/PlaceDetail/PlaceDetailView.swift +2 -2
- package/templates/ios/MapTemplate/MapTemplate/Features/Profile/ProfileView.swift +1 -1
- package/templates/ios/MapTemplate/MapTemplate/Features/Route/RouteView.swift +1 -1
- package/templates/ios/MapTemplate/MapTemplate/Features/Search/SearchView.swift +1 -1
- package/templates/ios/ShopTemplate/ShopTemplate/App/AppConfig.swift +9 -0
- package/templates/ios/ShopTemplate/ShopTemplate/Data/DataSourceResolver.swift +20 -0
- package/templates/ios/ShopTemplate/ShopTemplate/Features/Home/HomeView.swift +9 -7
- package/templates/ios/ShopTemplate/ShopTemplate/Features/Orders/OrdersView.swift +7 -3
- package/templates/ios/ShopTemplate/ShopTemplate/Features/Products/ProductDetailView.swift +4 -2
- package/templates/ios/ShopTemplate/ShopTemplate/Features/Products/ProductListView.swift +6 -3
- package/templates/ios/ShopTemplate/ShopTemplate/MockData/MockDataProvider.swift +1 -1
- package/templates/ios/Skeleton/Skeleton/App/AppConfig.swift +9 -0
- package/templates/ios/Skeleton/Skeleton/Data/DataRepository.swift +30 -0
- package/templates/ios/Skeleton/Skeleton/Data/DataSourceResolver.swift +20 -0
- package/templates/ios/Skeleton/Skeleton/Features/Explore/ExploreViewModel.swift +7 -6
- package/templates/ios/Skeleton/Skeleton/Features/Home/HomeViewModel.swift +8 -7
- package/templates/ios/Skeleton/Skeleton/MockData/MockDataProvider.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/App/AppConfig.swift +4 -3
- package/templates/ios/SocialTemplate/SocialTemplate/Data/DataRepository.swift +37 -0
- package/templates/ios/SocialTemplate/SocialTemplate/Data/DataSourceResolver.swift +20 -0
- package/templates/ios/SocialTemplate/SocialTemplate/Data/SyncManager.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/Features/CreatePost/CreatePostView.swift +2 -2
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Feed/CommentsView.swift +2 -2
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Feed/FeedView.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Messages/MessagesView.swift +4 -4
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Notifications/NotificationsView.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Profile/ProfileView.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Search/SearchView.swift +5 -4
- package/templates/ios/SocialTemplate/SocialTemplate/MockData/MockDataProvider.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/Models/Models.swift +2 -2
|
@@ -159,7 +159,7 @@ struct PlaceDetailView: View {
|
|
|
159
159
|
.foregroundColor(AppConfig.Theme.primaryColor)
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
let placeReviews =
|
|
162
|
+
let placeReviews = DataSourceResolver.repository.getReviews(for: place.id)
|
|
163
163
|
ForEach(Array(placeReviews.prefix(3).enumerated()), id: \.element.id) { index, review in
|
|
164
164
|
ReviewCardView(review: review, index: index)
|
|
165
165
|
.accessibilityIdentifier("place_detail_review_\(index)_card")
|
|
@@ -269,7 +269,7 @@ struct ReviewsListView: View {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
#Preview {
|
|
272
|
-
PlaceDetailView(place:
|
|
272
|
+
PlaceDetailView(place: DataSourceResolver.repository.places.first!)
|
|
273
273
|
.environmentObject(MapManager())
|
|
274
274
|
.environmentObject(AppState())
|
|
275
275
|
}
|
|
@@ -2,7 +2,7 @@ import SwiftUI
|
|
|
2
2
|
|
|
3
3
|
struct ProfileView: View {
|
|
4
4
|
@EnvironmentObject var appState: AppState
|
|
5
|
-
@State private var user =
|
|
5
|
+
@State private var user = DataSourceResolver.repository.currentUser
|
|
6
6
|
|
|
7
7
|
var body: some View {
|
|
8
8
|
NavigationStack {
|
|
@@ -94,7 +94,7 @@ struct RouteView: View {
|
|
|
94
94
|
|
|
95
95
|
private func loadRoute() {
|
|
96
96
|
isLoading = true
|
|
97
|
-
route =
|
|
97
|
+
route = DataSourceResolver.repository.getRoute(from: origin, to: destination)
|
|
98
98
|
isLoading = false
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -20,7 +20,7 @@ struct SearchView: View {
|
|
|
20
20
|
.accessibilityIdentifier("search_field")
|
|
21
21
|
.onChange(of: searchText) { newValue in
|
|
22
22
|
if !newValue.isEmpty {
|
|
23
|
-
suggestions =
|
|
23
|
+
suggestions = DataSourceResolver.repository.searchPlaces(query: newValue)
|
|
24
24
|
} else {
|
|
25
25
|
suggestions = []
|
|
26
26
|
}
|
|
@@ -108,6 +108,15 @@ struct AppConfig {
|
|
|
108
108
|
("Fast Delivery", "Get your orders delivered right to your doorstep", "shippingbox.fill")
|
|
109
109
|
]
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
// CUSTOMIZE:DATASOURCE - Data source configuration
|
|
113
|
+
struct DataSource {
|
|
114
|
+
enum SourceType {
|
|
115
|
+
case localStorage // Core Data — real local database (DEFAULT)
|
|
116
|
+
case mock // In-memory seed data — for development/testing only
|
|
117
|
+
}
|
|
118
|
+
static let active: SourceType = .mock
|
|
119
|
+
}
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
// MARK: - App State
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Resolves the active DataRepository implementation based on AppConfig.DataSource.active.
|
|
4
|
+
/// ViewModels use this to obtain the correct data source without knowing the concrete type.
|
|
5
|
+
///
|
|
6
|
+
/// Usage:
|
|
7
|
+
/// let repo = DataSourceResolver.repository
|
|
8
|
+
/// let products = repo.products
|
|
9
|
+
enum DataSourceResolver {
|
|
10
|
+
/// Returns the active DataRepository based on the current configuration.
|
|
11
|
+
static var repository: any DataRepository {
|
|
12
|
+
switch AppConfig.DataSource.active {
|
|
13
|
+
case .localStorage:
|
|
14
|
+
// TODO: Return LocalStorageProvider.shared once implemented
|
|
15
|
+
return MockDataProvider.shared
|
|
16
|
+
case .mock:
|
|
17
|
+
return MockDataProvider.shared
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -154,16 +154,18 @@ class HomeViewModel: ObservableObject {
|
|
|
154
154
|
var id: Self { self }
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
private let repository: any DataRepository
|
|
158
|
+
|
|
159
|
+
init(repository: any DataRepository = DataSourceResolver.repository) {
|
|
160
|
+
self.repository = repository
|
|
158
161
|
loadData()
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
func loadData() {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
saleProducts = mock.saleProducts()
|
|
165
|
+
banners = repository.banners
|
|
166
|
+
categories = repository.categories
|
|
167
|
+
featuredProducts = repository.featuredProducts()
|
|
168
|
+
saleProducts = repository.saleProducts()
|
|
167
169
|
loadRecentlyViewed()
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -179,7 +181,7 @@ class HomeViewModel: ObservableObject {
|
|
|
179
181
|
// Load from UserDefaults
|
|
180
182
|
if let data = UserDefaults.standard.data(forKey: "recentlyViewedIds"),
|
|
181
183
|
let ids = try? JSONDecoder().decode([String].self, from: data) {
|
|
182
|
-
recentlyViewed = ids.compactMap {
|
|
184
|
+
recentlyViewed = ids.compactMap { repository.product(by: $0) }
|
|
183
185
|
}
|
|
184
186
|
}
|
|
185
187
|
|
|
@@ -42,15 +42,19 @@ class OrdersViewModel: ObservableObject {
|
|
|
42
42
|
@Published var orders: [Order] = []
|
|
43
43
|
@Published var isLoading = true
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
private let repository: any DataRepository
|
|
46
|
+
|
|
47
|
+
init(repository: any DataRepository = DataSourceResolver.repository) {
|
|
48
|
+
self.repository = repository
|
|
46
49
|
loadOrders()
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
func loadOrders() {
|
|
50
53
|
// CUSTOMIZE:API - Replace with actual API call
|
|
51
54
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
52
|
-
self
|
|
53
|
-
self
|
|
55
|
+
guard let self = self else { return }
|
|
56
|
+
self.orders = self.repository.orders
|
|
57
|
+
self.isLoading = false
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
}
|
|
@@ -253,15 +253,17 @@ class ProductDetailViewModel: ObservableObject {
|
|
|
253
253
|
@Published var isInWishlist = false
|
|
254
254
|
|
|
255
255
|
private let wishlistManager = WishlistManager()
|
|
256
|
+
private let repository: any DataRepository
|
|
256
257
|
|
|
257
|
-
init(product: Product) {
|
|
258
|
+
init(product: Product, repository: any DataRepository = DataSourceResolver.repository) {
|
|
258
259
|
self.product = product
|
|
260
|
+
self.repository = repository
|
|
259
261
|
self.isInWishlist = wishlistManager.isInWishlist(product)
|
|
260
262
|
loadReviews()
|
|
261
263
|
}
|
|
262
264
|
|
|
263
265
|
func loadReviews() {
|
|
264
|
-
reviews =
|
|
266
|
+
reviews = repository.reviews(for: product.id)
|
|
265
267
|
}
|
|
266
268
|
|
|
267
269
|
func toggleWishlist() {
|
|
@@ -222,8 +222,11 @@ class ProductListViewModel: ObservableObject {
|
|
|
222
222
|
return result
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
private let repository: any DataRepository
|
|
226
|
+
|
|
227
|
+
init(category: Category?, products: [Product]? = nil, repository: any DataRepository = DataSourceResolver.repository) {
|
|
226
228
|
self.category = category
|
|
229
|
+
self.repository = repository
|
|
227
230
|
if let products = products {
|
|
228
231
|
self.products = products
|
|
229
232
|
} else {
|
|
@@ -237,9 +240,9 @@ class ProductListViewModel: ObservableObject {
|
|
|
237
240
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
238
241
|
guard let self = self else { return }
|
|
239
242
|
if let category = self.category {
|
|
240
|
-
self.products =
|
|
243
|
+
self.products = self.repository.products(for: category)
|
|
241
244
|
} else {
|
|
242
|
-
self.products =
|
|
245
|
+
self.products = self.repository.products
|
|
243
246
|
}
|
|
244
247
|
self.isLoading = false
|
|
245
248
|
}
|
|
@@ -5,7 +5,7 @@ import Combine
|
|
|
5
5
|
// CUSTOMIZE:API - Replace mock data with real API calls
|
|
6
6
|
// Mutable observable singleton. Seed data lives in `default*` static properties.
|
|
7
7
|
// The agent customizes seed data; CRUD methods and infrastructure are generic.
|
|
8
|
-
class MockDataProvider: ObservableObject {
|
|
8
|
+
class MockDataProvider: ObservableObject, DataRepository {
|
|
9
9
|
static let shared = MockDataProvider()
|
|
10
10
|
|
|
11
11
|
// MARK: - Published Mutable State
|
|
@@ -83,6 +83,15 @@ struct AppConfig {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// CUSTOMIZE:DATASOURCE - Data source configuration
|
|
87
|
+
struct DataSource {
|
|
88
|
+
enum SourceType {
|
|
89
|
+
case localStorage // Core Data — real local database (DEFAULT)
|
|
90
|
+
case mock // In-memory seed data — for development/testing only
|
|
91
|
+
}
|
|
92
|
+
static let active: SourceType = .mock
|
|
93
|
+
}
|
|
94
|
+
|
|
86
95
|
// CUSTOMIZE:CATEGORIES - Domain categories for content
|
|
87
96
|
static let categories: [(name: String, icon: String)] = [
|
|
88
97
|
("Category 1", "folder.fill"),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Protocol abstracting the data layer. Both MockDataProvider and LocalStorageProvider
|
|
4
|
+
/// conform to this, allowing the app to swap data sources via AppConfig.DataSource.active.
|
|
5
|
+
///
|
|
6
|
+
/// ViewModels depend on this protocol (via `any DataRepository`) rather than on a concrete
|
|
7
|
+
/// implementation. Use `DataSourceResolver.repository` to get the active provider.
|
|
8
|
+
protocol DataRepository: AnyObject {
|
|
9
|
+
// MARK: - Observable State
|
|
10
|
+
|
|
11
|
+
var items: [Item] { get }
|
|
12
|
+
var categories: [Category] { get }
|
|
13
|
+
var favoriteItems: [Item] { get }
|
|
14
|
+
|
|
15
|
+
// MARK: - Data Loading
|
|
16
|
+
|
|
17
|
+
func loadData()
|
|
18
|
+
|
|
19
|
+
// MARK: - CRUD Operations
|
|
20
|
+
|
|
21
|
+
func addItem(_ item: Item)
|
|
22
|
+
func updateItem(_ item: Item)
|
|
23
|
+
func deleteItem(_ item: Item)
|
|
24
|
+
func toggleFavorite(_ item: Item)
|
|
25
|
+
|
|
26
|
+
// MARK: - Queries
|
|
27
|
+
|
|
28
|
+
func searchItems(query: String) -> [Item]
|
|
29
|
+
func itemsByCategory(_ categoryName: String) -> [Item]
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Resolves the active DataRepository implementation based on AppConfig.DataSource.active.
|
|
4
|
+
/// ViewModels use this to obtain the correct data source without knowing the concrete type.
|
|
5
|
+
///
|
|
6
|
+
/// Usage:
|
|
7
|
+
/// let repo = DataSourceResolver.repository
|
|
8
|
+
/// let items = repo.items
|
|
9
|
+
enum DataSourceResolver {
|
|
10
|
+
/// Returns the active DataRepository based on the current configuration.
|
|
11
|
+
static var repository: any DataRepository {
|
|
12
|
+
switch AppConfig.DataSource.active {
|
|
13
|
+
case .localStorage:
|
|
14
|
+
// TODO: Return LocalStorageProvider.shared once implemented
|
|
15
|
+
return MockDataProvider.shared
|
|
16
|
+
case .mock:
|
|
17
|
+
return MockDataProvider.shared
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -6,21 +6,22 @@ class ExploreViewModel: ObservableObject {
|
|
|
6
6
|
@Published var categories: [Category] = []
|
|
7
7
|
@Published var searchResults: [Item] = []
|
|
8
8
|
|
|
9
|
-
private let
|
|
9
|
+
private let repository: any DataRepository
|
|
10
10
|
|
|
11
|
-
init() {
|
|
12
|
-
|
|
11
|
+
init(repository: any DataRepository = DataSourceResolver.repository) {
|
|
12
|
+
self.repository = repository
|
|
13
|
+
categories = repository.categories
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
func search(query: String) {
|
|
16
|
-
searchResults =
|
|
17
|
+
searchResults = repository.searchItems(query: query)
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
func itemsForCategory(_ name: String) -> [Item] {
|
|
20
|
-
|
|
21
|
+
repository.itemsByCategory(name)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
func toggleFavorite(_ item: Item) {
|
|
24
|
-
|
|
25
|
+
repository.toggleFavorite(item)
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -9,20 +9,21 @@ class HomeViewModel: ObservableObject {
|
|
|
9
9
|
@Published var filteredItems: [Item] = []
|
|
10
10
|
@Published var selectedCategory: Category?
|
|
11
11
|
|
|
12
|
-
private let
|
|
12
|
+
private let repository: any DataRepository
|
|
13
13
|
|
|
14
|
-
init() {
|
|
14
|
+
init(repository: any DataRepository = DataSourceResolver.repository) {
|
|
15
|
+
self.repository = repository
|
|
15
16
|
loadData()
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
func loadData() {
|
|
19
|
-
items =
|
|
20
|
-
categories =
|
|
20
|
+
items = repository.items
|
|
21
|
+
categories = repository.categories
|
|
21
22
|
filteredItems = items
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
func refresh() {
|
|
25
|
-
|
|
26
|
+
repository.loadData()
|
|
26
27
|
loadData()
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -30,7 +31,7 @@ class HomeViewModel: ObservableObject {
|
|
|
30
31
|
if query.isEmpty {
|
|
31
32
|
applyFilters()
|
|
32
33
|
} else {
|
|
33
|
-
let searched =
|
|
34
|
+
let searched = repository.searchItems(query: query)
|
|
34
35
|
if let cat = selectedCategory {
|
|
35
36
|
filteredItems = searched.filter { $0.category == cat.name }
|
|
36
37
|
} else {
|
|
@@ -49,7 +50,7 @@ class HomeViewModel: ObservableObject {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
func toggleFavorite(_ item: Item) {
|
|
52
|
-
|
|
53
|
+
repository.toggleFavorite(item)
|
|
53
54
|
loadData()
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -4,7 +4,7 @@ import SwiftUI
|
|
|
4
4
|
// PLACEHOLDER: Replace sample data with domain-specific content.
|
|
5
5
|
// Keep the singleton pattern and CRUD interface — just change the data.
|
|
6
6
|
|
|
7
|
-
class MockDataProvider: ObservableObject {
|
|
7
|
+
class MockDataProvider: ObservableObject, DataRepository {
|
|
8
8
|
static let shared = MockDataProvider()
|
|
9
9
|
|
|
10
10
|
@Published var items: [Item] = []
|
|
@@ -111,7 +111,7 @@ class AuthManager: ObservableObject {
|
|
|
111
111
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
|
112
112
|
|
|
113
113
|
await MainActor.run {
|
|
114
|
-
currentUser =
|
|
114
|
+
currentUser = DataSourceResolver.repository.currentUser
|
|
115
115
|
isAuthenticated = true
|
|
116
116
|
isLoading = false
|
|
117
117
|
}
|
|
@@ -136,8 +136,9 @@ class FeedManager: ObservableObject {
|
|
|
136
136
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
|
137
137
|
|
|
138
138
|
await MainActor.run {
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
let repo = DataSourceResolver.repository
|
|
140
|
+
posts = repo.posts
|
|
141
|
+
stories = repo.stories
|
|
141
142
|
isLoading = false
|
|
142
143
|
}
|
|
143
144
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Protocol abstracting the data layer. Both MockDataProvider and LocalStorageProvider
|
|
4
|
+
/// conform to this, allowing the app to swap data sources via AppConfig.DataSource.active.
|
|
5
|
+
///
|
|
6
|
+
/// ViewModels depend on this protocol (via `any DataRepository`) rather than on a concrete
|
|
7
|
+
/// implementation. Use `DataSourceResolver.repository` to get the active provider.
|
|
8
|
+
protocol DataRepository: AnyObject {
|
|
9
|
+
// MARK: - Observable State
|
|
10
|
+
|
|
11
|
+
var currentUser: User { get }
|
|
12
|
+
var users: [User] { get }
|
|
13
|
+
var posts: [Post] { get }
|
|
14
|
+
var stories: [Story] { get }
|
|
15
|
+
var conversations: [Conversation] { get }
|
|
16
|
+
var notifications: [AppNotification] { get }
|
|
17
|
+
var trendingHashtags: [Hashtag] { get }
|
|
18
|
+
|
|
19
|
+
// MARK: - Helper Functions
|
|
20
|
+
|
|
21
|
+
func comments(for postId: String) -> [Comment]
|
|
22
|
+
|
|
23
|
+
// MARK: - CRUD Operations
|
|
24
|
+
|
|
25
|
+
func addPost(_ post: Post)
|
|
26
|
+
func likePost(_ postId: String)
|
|
27
|
+
func unlikePost(_ postId: String)
|
|
28
|
+
func savePost(_ postId: String)
|
|
29
|
+
func unsavePost(_ postId: String)
|
|
30
|
+
func addComment(_ comment: Comment, to postId: String)
|
|
31
|
+
func likeComment(_ commentId: String, in postId: String)
|
|
32
|
+
func followUser(_ userId: String)
|
|
33
|
+
func unfollowUser(_ userId: String)
|
|
34
|
+
func markNotificationAsRead(_ notificationId: String)
|
|
35
|
+
func sendMessage(_ message: Message, in conversationId: String)
|
|
36
|
+
func reset()
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Resolves the active DataRepository implementation based on AppConfig.DataSource.active.
|
|
4
|
+
/// ViewModels use this to obtain the correct data source without knowing the concrete type.
|
|
5
|
+
///
|
|
6
|
+
/// Usage:
|
|
7
|
+
/// let repo = DataSourceResolver.repository
|
|
8
|
+
/// let posts = repo.posts
|
|
9
|
+
enum DataSourceResolver {
|
|
10
|
+
/// Returns the active DataRepository based on the current configuration.
|
|
11
|
+
static var repository: any DataRepository {
|
|
12
|
+
switch AppConfig.DataSource.active {
|
|
13
|
+
case .localStorage:
|
|
14
|
+
// TODO: Return LocalStorageProvider.shared once implemented
|
|
15
|
+
return MockDataProvider.shared
|
|
16
|
+
case .mock:
|
|
17
|
+
return MockDataProvider.shared
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -7,7 +7,7 @@ import Combine
|
|
|
7
7
|
class SyncManager: ObservableObject {
|
|
8
8
|
static let shared = SyncManager()
|
|
9
9
|
|
|
10
|
-
private let store =
|
|
10
|
+
private let store: any DataRepository = DataSourceResolver.repository
|
|
11
11
|
private let network = NetworkMonitor.shared
|
|
12
12
|
|
|
13
13
|
@Published var isSyncing: Bool = false
|
package/templates/ios/SocialTemplate/SocialTemplate/Features/CreatePost/CreatePostView.swift
CHANGED
|
@@ -18,7 +18,7 @@ struct CreatePostView: View {
|
|
|
18
18
|
VStack(alignment: .leading, spacing: 16) {
|
|
19
19
|
// User Header
|
|
20
20
|
HStack(spacing: 12) {
|
|
21
|
-
AsyncImage(url: URL(string:
|
|
21
|
+
AsyncImage(url: URL(string: DataSourceResolver.repository.currentUser.avatarURL ?? "")) { image in
|
|
22
22
|
image
|
|
23
23
|
.resizable()
|
|
24
24
|
.aspectRatio(contentMode: .fill)
|
|
@@ -30,7 +30,7 @@ struct CreatePostView: View {
|
|
|
30
30
|
.clipShape(Circle())
|
|
31
31
|
|
|
32
32
|
VStack(alignment: .leading) {
|
|
33
|
-
Text(
|
|
33
|
+
Text(DataSourceResolver.repository.currentUser.username)
|
|
34
34
|
.font(.subheadline)
|
|
35
35
|
.fontWeight(.semibold)
|
|
36
36
|
|
|
@@ -23,7 +23,7 @@ struct CommentsView: View {
|
|
|
23
23
|
|
|
24
24
|
// Comment Input
|
|
25
25
|
HStack(spacing: 12) {
|
|
26
|
-
AsyncImage(url: URL(string:
|
|
26
|
+
AsyncImage(url: URL(string: DataSourceResolver.repository.currentUser.avatarURL ?? "")) { image in
|
|
27
27
|
image
|
|
28
28
|
.resizable()
|
|
29
29
|
.aspectRatio(contentMode: .fill)
|
|
@@ -57,7 +57,7 @@ struct CommentsView: View {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
.onAppear {
|
|
60
|
-
comments =
|
|
60
|
+
comments = DataSourceResolver.repository.comments(for: post.id)
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -58,7 +58,7 @@ struct StoriesBarView: View {
|
|
|
58
58
|
// Add story button
|
|
59
59
|
VStack(spacing: 4) {
|
|
60
60
|
ZStack(alignment: .bottomTrailing) {
|
|
61
|
-
AsyncImage(url: URL(string:
|
|
61
|
+
AsyncImage(url: URL(string: DataSourceResolver.repository.currentUser.avatarURL ?? "")) { image in
|
|
62
62
|
image
|
|
63
63
|
.resizable()
|
|
64
64
|
.aspectRatio(contentMode: .fill)
|
|
@@ -40,7 +40,7 @@ struct MessagesView: View {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
.onAppear {
|
|
43
|
-
conversations =
|
|
43
|
+
conversations = DataSourceResolver.repository.conversations
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -113,7 +113,7 @@ struct ChatView: View {
|
|
|
113
113
|
ForEach(messages) { message in
|
|
114
114
|
MessageBubble(
|
|
115
115
|
message: message,
|
|
116
|
-
isFromCurrentUser: message.senderId ==
|
|
116
|
+
isFromCurrentUser: message.senderId == DataSourceResolver.repository.currentUser.id
|
|
117
117
|
)
|
|
118
118
|
}
|
|
119
119
|
}
|
|
@@ -187,7 +187,7 @@ struct ChatView: View {
|
|
|
187
187
|
messages = (1...20).map { i in
|
|
188
188
|
Message(
|
|
189
189
|
id: "m\(i)",
|
|
190
|
-
senderId: i % 3 == 0 ?
|
|
190
|
+
senderId: i % 3 == 0 ? DataSourceResolver.repository.currentUser.id : conversation.participants.first!.id,
|
|
191
191
|
content: ["Hey!", "How are you?", "Just saw your post!", "That looks amazing!", "Let's catch up soon"][i % 5],
|
|
192
192
|
mediaURL: nil,
|
|
193
193
|
mediaType: .none,
|
|
@@ -200,7 +200,7 @@ struct ChatView: View {
|
|
|
200
200
|
private func sendMessage() {
|
|
201
201
|
let message = Message(
|
|
202
202
|
id: UUID().uuidString,
|
|
203
|
-
senderId:
|
|
203
|
+
senderId: DataSourceResolver.repository.currentUser.id,
|
|
204
204
|
content: newMessage,
|
|
205
205
|
mediaURL: nil,
|
|
206
206
|
mediaType: .none,
|
|
@@ -9,7 +9,7 @@ struct ProfileView: View {
|
|
|
9
9
|
@State private var showEditProfile = false
|
|
10
10
|
|
|
11
11
|
private var isOwnProfile: Bool {
|
|
12
|
-
userId == nil || userId ==
|
|
12
|
+
userId == nil || userId == DataSourceResolver.repository.currentUser.id
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
var body: some View {
|
|
@@ -47,7 +47,8 @@ struct SearchView: View {
|
|
|
47
47
|
return
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
let repo = DataSourceResolver.repository
|
|
51
|
+
searchResults = repo.users.filter {
|
|
51
52
|
$0.username.lowercased().contains(query.lowercased()) ||
|
|
52
53
|
$0.displayName.lowercased().contains(query.lowercased())
|
|
53
54
|
}
|
|
@@ -61,7 +62,7 @@ struct TrendingHashtagsSection: View {
|
|
|
61
62
|
.font(.headline)
|
|
62
63
|
.padding(.horizontal)
|
|
63
64
|
|
|
64
|
-
ForEach(
|
|
65
|
+
ForEach(DataSourceResolver.repository.trendingHashtags.prefix(5)) { hashtag in
|
|
65
66
|
HStack {
|
|
66
67
|
VStack(alignment: .leading, spacing: 2) {
|
|
67
68
|
Text(hashtag.name)
|
|
@@ -103,7 +104,7 @@ struct SuggestedUsersSection: View {
|
|
|
103
104
|
|
|
104
105
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
105
106
|
HStack(spacing: 12) {
|
|
106
|
-
ForEach(
|
|
107
|
+
ForEach(DataSourceResolver.repository.users.prefix(5)) { user in
|
|
107
108
|
SuggestedUserCard(user: user)
|
|
108
109
|
}
|
|
109
110
|
}
|
|
@@ -177,7 +178,7 @@ struct ExploreGridSection: View {
|
|
|
177
178
|
.padding(.horizontal)
|
|
178
179
|
|
|
179
180
|
LazyVGrid(columns: columns, spacing: 2) {
|
|
180
|
-
ForEach(Array(
|
|
181
|
+
ForEach(Array(DataSourceResolver.repository.posts.enumerated()), id: \.element.id) { index, post in
|
|
181
182
|
NavigationLink(destination: PostDetailView(post: post)) {
|
|
182
183
|
AsyncImage(url: URL(string: post.mediaURLs.first ?? "")) { image in
|
|
183
184
|
image
|
|
@@ -104,14 +104,14 @@ struct Conversation: Codable, Identifiable {
|
|
|
104
104
|
if isGroup {
|
|
105
105
|
return groupName ?? participants.map { $0.displayName }.joined(separator: ", ")
|
|
106
106
|
}
|
|
107
|
-
return participants.first { $0.id !=
|
|
107
|
+
return participants.first { $0.id != DataSourceResolver.repository.currentUser.id }?.displayName ?? ""
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
var avatarURL: String? {
|
|
111
111
|
if isGroup {
|
|
112
112
|
return groupAvatarURL
|
|
113
113
|
}
|
|
114
|
-
return participants.first { $0.id !=
|
|
114
|
+
return participants.first { $0.id != DataSourceResolver.repository.currentUser.id }?.avatarURL
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|