Skip to content

Commit 9657d5c

Browse files
authored
Merge pull request #10 from nativeapptemplate/add_shopViewModel
Add shop view model
2 parents 58db399 + 5b6a0c1 commit 9657d5c

File tree

15 files changed

+1044
-44
lines changed

15 files changed

+1044
-44
lines changed

NativeAppTemplate.xcodeproj/project.pbxproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
010F86BE2622F9C900B6C62A /* ShopListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010F86BD2622F9C900B6C62A /* ShopListView.swift */; };
1818
0110A15F2AC816F5003EDCBA /* SendConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0110A15E2AC816F5003EDCBA /* SendConfirmation.swift */; };
1919
0110A1612AC81978003EDCBA /* ResendConfirmationInstructionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0110A1602AC81978003EDCBA /* ResendConfirmationInstructionsView.swift */; };
20+
0114F3AC2E079BD100F4A1DD /* ShopListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0114F3AB2E079BD100F4A1DD /* ShopListViewModel.swift */; };
2021
011586122B567363005E8E8F /* SignUpOrSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011586112B567363005E8E8F /* SignUpOrSignInView.swift */; };
2122
011DDC21287669EA00C6C21F /* SignUpRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DDC20287669EA00C6C21F /* SignUpRepository.swift */; };
2223
011DDC2328766C5E00C6C21F /* SignUpService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DDC2228766C5D00C6C21F /* SignUpService.swift */; };
@@ -172,6 +173,7 @@
172173
010F86BD2622F9C900B6C62A /* ShopListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopListView.swift; sourceTree = "<group>"; };
173174
0110A15E2AC816F5003EDCBA /* SendConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendConfirmation.swift; sourceTree = "<group>"; };
174175
0110A1602AC81978003EDCBA /* ResendConfirmationInstructionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResendConfirmationInstructionsView.swift; sourceTree = "<group>"; };
176+
0114F3AB2E079BD100F4A1DD /* ShopListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopListViewModel.swift; sourceTree = "<group>"; };
175177
011586112B567363005E8E8F /* SignUpOrSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpOrSignInView.swift; sourceTree = "<group>"; };
176178
011DDC20287669EA00C6C21F /* SignUpRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpRepository.swift; sourceTree = "<group>"; };
177179
011DDC2228766C5D00C6C21F /* SignUpService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpService.swift; sourceTree = "<group>"; };
@@ -439,6 +441,7 @@
439441
013DE734284E99DF00528CC5 /* ShopCreateView.swift */,
440442
01DCE23E298FA3B300BA311D /* ShopListCardView.swift */,
441443
010F86BD2622F9C900B6C62A /* ShopListView.swift */,
444+
0114F3AB2E079BD100F4A1DD /* ShopListViewModel.swift */,
442445
017278952D7D99D100CE424F /* ItemTag Detail */,
443446
017278992D7D99D100CE424F /* ItemTag List */,
444447
);
@@ -914,6 +917,7 @@
914917
"",
915918
"",
916919
"",
920+
"",
917921
);
918922
};
919923
/* End PBXShellScriptBuildPhase section */
@@ -969,6 +973,7 @@
969973
011DDC21287669EA00C6C21F /* SignUpRepository.swift in Sources */,
970974
0172035B25A9642E008FD63B /* Service.swift in Sources */,
971975
01E0A5B625BD0FCD00298D35 /* LoadingView.swift in Sources */,
976+
0114F3AC2E079BD100F4A1DD /* ShopListViewModel.swift in Sources */,
972977
0172051A25AAF6C0008FD63B /* SessionsService.swift in Sources */,
973978
017204D125AA8479008FD63B /* DataState.swift in Sources */,
974979
012643372B3554AD00D4E9BD /* AcceptTermsView.swift in Sources */,

NativeAppTemplate/UI/App Root/MainView.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,14 @@ private extension MainView {
148148
}
149149

150150
func shopListView() -> ShopListView {
151-
.init(
151+
let viewModel = ShopListViewModel(
152+
sessionController: sessionController,
152153
shopRepository: dataManager.shopRepository,
153-
itemTagRepository: dataManager.itemTagRepository
154+
itemTagRepository: dataManager.itemTagRepository,
155+
tabViewModel: tabViewModel,
156+
mainTab: .shops
154157
)
158+
return ShopListView(viewModel: viewModel)
155159
}
156160

157161
func scanView() -> ScanView {

NativeAppTemplate/UI/Shop List/ShopListView.swift

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -44,41 +44,32 @@ struct TapShopBelowTip: Tip {
4444
}
4545

4646
struct ShopListView: View {
47-
@Environment(\.mainTab) private var mainTab
48-
@Environment(TabViewModel.self) private var tabViewModel
49-
@Environment(\.sessionController) private var sessionController
50-
private var shopRepository: ShopRepositoryProtocol
51-
private var itemTagRepository: ItemTagRepositoryProtocol
52-
@State private var isShowingCreateSheet = false
47+
@State private var viewModel: ShopListViewModel
5348

54-
init(
55-
shopRepository: ShopRepositoryProtocol,
56-
itemTagRepository: ItemTagRepositoryProtocol
57-
) {
58-
self.shopRepository = shopRepository
59-
self.itemTagRepository = itemTagRepository
49+
init(viewModel: ShopListViewModel) {
50+
self._viewModel = State(wrappedValue: viewModel)
6051
}
6152
}
6253

6354
extension ShopListView {
6455
var body: some View {
6556
contentView
6657
.task {
67-
reload()
58+
viewModel.reload()
6859
}
6960
.onAppear {
70-
tabViewModel.showingDetailView[mainTab] = false
61+
viewModel.setTabViewModelShowingDetailViewToFalse()
7162
}
72-
.onChange(of: shopRepository.state) {
73-
if shopRepository.state == .initial {
74-
reload()
63+
.onChange(of: viewModel.state) {
64+
if viewModel.state == .initial {
65+
viewModel.reload()
7566
}
7667
}
7768
// Avoid showing deleted shop.
78-
.onChange(of: sessionController.shouldPopToRootView) {
69+
.onChange(of: viewModel.shouldPopToRootView) {
7970
Task {
8071
try await Task.sleep(nanoseconds: 2_000_000_000)
81-
reload()
72+
viewModel.reload()
8273
}
8374
}
8475
}
@@ -88,7 +79,7 @@ extension ShopListView {
8879
private extension ShopListView {
8980
var contentView: some View {
9081
@ViewBuilder var contentView: some View {
91-
switch shopRepository.state {
82+
switch viewModel.state {
9283
case .initial, .loading:
9384
LoadingView()
9485
case .hasData:
@@ -101,12 +92,8 @@ private extension ShopListView {
10192
return contentView
10293
}
10394

104-
func reload() {
105-
shopRepository.reload()
106-
}
107-
10895
var cardsView: some View {
109-
ForEach(shopRepository.shops) { shop in
96+
ForEach(viewModel.shops) { shop in
11097
NavigationLink(value: shop) {
11198
ShopListCardView(shop: shop)
11299
}
@@ -115,11 +102,9 @@ private extension ShopListView {
115102
}
116103

117104
var shopListView: some View {
118-
let leftInShopSlots = shopRepository.limitCount - shopRepository.createdShopsCount
119-
120-
return VStack {
121-
if shopRepository.isEmpty {
122-
noResultsView(leftInShopSlots: leftInShopSlots)
105+
VStack {
106+
if viewModel.isEmpty {
107+
noResultsView(leftInShopSlots: viewModel.leftInShopSlots)
123108
} else {
124109
List {
125110
Section {
@@ -130,11 +115,11 @@ private extension ShopListView {
130115
.tint(.alarm)
131116

132117
EmptyView()
133-
.id(ScrollToTopID(mainTab: mainTab, detail: false))
118+
.id(viewModel.scrollToTopID())
134119
} footer: {
135120
VStack(spacing: 0) {
136121
HStack(alignment: .firstTextBaseline) {
137-
Text(String(leftInShopSlots))
122+
Text(String(viewModel.leftInShopSlots))
138123
.font(.uiLabelBold)
139124
Text(verbatim: "left in shop slots.")
140125
.font(.uiFootnote)
@@ -144,41 +129,41 @@ private extension ShopListView {
144129
}
145130
.navigationDestination(for: Shop.self) { shop in
146131
ShopDetailView(
147-
shopRepository: shopRepository,
148-
itemTagRepository: itemTagRepository,
132+
shopRepository: viewModel.shopRepository,
133+
itemTagRepository: viewModel.itemTagRepository,
149134
shopId: shop.id
150135
)
151136
}
152137
.accessibility(identifier: "shopListView")
153138
.refreshable {
154-
reload()
139+
viewModel.reload()
155140
}
156141
}
157142
}
158143
.navigationTitle(String.shops)
159144
.navigationBarTitleDisplayMode(.inline)
160145
.toolbar {
161-
if leftInShopSlots > 0 {
146+
if viewModel.leftInShopSlots > 0 {
162147
ToolbarItem(placement: .navigationBarTrailing) {
163148
Button {
164-
isShowingCreateSheet.toggle()
149+
viewModel.showCreateView()
165150
} label: {
166151
Image(systemName: "plus")
167152
}
168153
}
169154
}
170155
}
171-
.sheet(isPresented: $isShowingCreateSheet,
156+
.sheet(isPresented: $viewModel.isShowingCreateSheet,
172157
onDismiss: {
173-
reload()
158+
viewModel.reload()
174159
}, content: {
175-
ShopCreateView(shopRepository: shopRepository)
160+
ShopCreateView(shopRepository: viewModel.shopRepository)
176161
}
177162
)
178163
}
179164

180165
var reloadView: some View {
181-
ErrorView(buttonAction: reload)
166+
ErrorView(buttonAction: viewModel.reload)
182167
}
183168

184169
func noResultsView(leftInShopSlots: Int) -> some View {
@@ -195,7 +180,7 @@ private extension ShopListView {
195180
.padding()
196181

197182
MainButtonView(title: String.addShop, type: .primary(withArrow: false)) {
198-
isShowingCreateSheet.toggle()
183+
viewModel.showCreateView()
199184
}
200185
.padding()
201186

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// ShopListViewModel.swift
3+
// NativeAppTemplate
4+
//
5+
// Created by Claude on 2025/06/22.
6+
//
7+
8+
import SwiftUI
9+
import Observation
10+
11+
@Observable
12+
@MainActor
13+
final class ShopListViewModel {
14+
var isShowingCreateSheet = false
15+
16+
var state: DataState { shopRepository.state }
17+
var shops: [Shop] { shopRepository.shops }
18+
var limitCount: Int { shopRepository.limitCount }
19+
var createdShopsCount: Int { shopRepository.createdShopsCount }
20+
var isEmpty: Bool { shopRepository.isEmpty }
21+
var leftInShopSlots: Int { limitCount - createdShopsCount }
22+
var shouldPopToRootView: Bool { sessionController.shouldPopToRootView }
23+
24+
let shopRepository: ShopRepositoryProtocol
25+
let itemTagRepository: ItemTagRepositoryProtocol
26+
private let sessionController: SessionControllerProtocol
27+
private let tabViewModel: TabViewModel
28+
private let mainTab: MainTab
29+
30+
init(
31+
sessionController: SessionControllerProtocol,
32+
shopRepository: ShopRepositoryProtocol,
33+
itemTagRepository: ItemTagRepositoryProtocol,
34+
tabViewModel: TabViewModel,
35+
mainTab: MainTab
36+
) {
37+
self.sessionController = sessionController
38+
self.shopRepository = shopRepository
39+
self.itemTagRepository = itemTagRepository
40+
self.tabViewModel = tabViewModel
41+
self.mainTab = mainTab
42+
}
43+
44+
func reload() {
45+
shopRepository.reload()
46+
}
47+
48+
func showCreateView() {
49+
isShowingCreateSheet.toggle()
50+
}
51+
52+
func setTabViewModelShowingDetailViewToFalse() {
53+
tabViewModel.showingDetailView[mainTab] = false
54+
}
55+
56+
func scrollToTopID() -> ScrollToTopID {
57+
ScrollToTopID(mainTab: mainTab, detail: false)
58+
}
59+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// DemoOnboardingRepository.swift
3+
// NativeAppTemplate
4+
//
5+
// Created by Claude on 2025/06/22.
6+
//
7+
8+
@testable import NativeAppTemplate
9+
import Foundation
10+
import OrderedCollections
11+
12+
@MainActor
13+
final class DemoOnboardingRepository: OnboardingRepositoryProtocol {
14+
var onboardings: [Onboarding] = []
15+
var onboardingsDictionary: OrderedDictionary<Int, Bool> {
16+
var dict = OrderedDictionary<Int, Bool>()
17+
for onboarding in onboardings {
18+
dict[onboarding.id] = onboarding.isPortraitImage
19+
}
20+
return dict
21+
}
22+
23+
func reload() {
24+
// Demo data with predefined onboarding items
25+
let demoOnboardingData: OrderedDictionary = [
26+
1: false, // Landscape image
27+
2: false, // Landscape image
28+
3: false, // Landscape image
29+
4: true, // Portrait image
30+
5: false, // Landscape image
31+
6: false, // Landscape image
32+
7: true, // Portrait image
33+
8: true, // Portrait image
34+
9: false, // Landscape image
35+
10: false, // Landscape image
36+
11: true, // Portrait image
37+
12: false, // Landscape image
38+
13: false // Landscape image
39+
]
40+
41+
onboardings = demoOnboardingData.map { key, value in
42+
Onboarding(id: key, isPortraitImage: value)
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)