Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions NativeAppTemplate.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
010F86BE2622F9C900B6C62A /* ShopListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010F86BD2622F9C900B6C62A /* ShopListView.swift */; };
0110A15F2AC816F5003EDCBA /* SendConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0110A15E2AC816F5003EDCBA /* SendConfirmation.swift */; };
0110A1612AC81978003EDCBA /* ResendConfirmationInstructionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0110A1602AC81978003EDCBA /* ResendConfirmationInstructionsView.swift */; };
0114F3AC2E079BD100F4A1DD /* ShopListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0114F3AB2E079BD100F4A1DD /* ShopListViewModel.swift */; };
011586122B567363005E8E8F /* SignUpOrSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011586112B567363005E8E8F /* SignUpOrSignInView.swift */; };
011DDC21287669EA00C6C21F /* SignUpRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DDC20287669EA00C6C21F /* SignUpRepository.swift */; };
011DDC2328766C5E00C6C21F /* SignUpService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DDC2228766C5D00C6C21F /* SignUpService.swift */; };
Expand Down Expand Up @@ -172,6 +173,7 @@
010F86BD2622F9C900B6C62A /* ShopListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopListView.swift; sourceTree = "<group>"; };
0110A15E2AC816F5003EDCBA /* SendConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendConfirmation.swift; sourceTree = "<group>"; };
0110A1602AC81978003EDCBA /* ResendConfirmationInstructionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResendConfirmationInstructionsView.swift; sourceTree = "<group>"; };
0114F3AB2E079BD100F4A1DD /* ShopListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopListViewModel.swift; sourceTree = "<group>"; };
011586112B567363005E8E8F /* SignUpOrSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpOrSignInView.swift; sourceTree = "<group>"; };
011DDC20287669EA00C6C21F /* SignUpRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpRepository.swift; sourceTree = "<group>"; };
011DDC2228766C5D00C6C21F /* SignUpService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -439,6 +441,7 @@
013DE734284E99DF00528CC5 /* ShopCreateView.swift */,
01DCE23E298FA3B300BA311D /* ShopListCardView.swift */,
010F86BD2622F9C900B6C62A /* ShopListView.swift */,
0114F3AB2E079BD100F4A1DD /* ShopListViewModel.swift */,
017278952D7D99D100CE424F /* ItemTag Detail */,
017278992D7D99D100CE424F /* ItemTag List */,
);
Expand Down Expand Up @@ -914,6 +917,7 @@
"",
"",
"",
"",
);
};
/* End PBXShellScriptBuildPhase section */
Expand Down Expand Up @@ -969,6 +973,7 @@
011DDC21287669EA00C6C21F /* SignUpRepository.swift in Sources */,
0172035B25A9642E008FD63B /* Service.swift in Sources */,
01E0A5B625BD0FCD00298D35 /* LoadingView.swift in Sources */,
0114F3AC2E079BD100F4A1DD /* ShopListViewModel.swift in Sources */,
0172051A25AAF6C0008FD63B /* SessionsService.swift in Sources */,
017204D125AA8479008FD63B /* DataState.swift in Sources */,
012643372B3554AD00D4E9BD /* AcceptTermsView.swift in Sources */,
Expand Down
8 changes: 6 additions & 2 deletions NativeAppTemplate/UI/App Root/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,14 @@ private extension MainView {
}

func shopListView() -> ShopListView {
.init(
let viewModel = ShopListViewModel(
sessionController: sessionController,
shopRepository: dataManager.shopRepository,
itemTagRepository: dataManager.itemTagRepository
itemTagRepository: dataManager.itemTagRepository,
tabViewModel: tabViewModel,
mainTab: .shops
)
return ShopListView(viewModel: viewModel)
}

func scanView() -> ScanView {
Expand Down
69 changes: 27 additions & 42 deletions NativeAppTemplate/UI/Shop List/ShopListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,41 +44,32 @@ struct TapShopBelowTip: Tip {
}

struct ShopListView: View {
@Environment(\.mainTab) private var mainTab
@Environment(TabViewModel.self) private var tabViewModel
@Environment(\.sessionController) private var sessionController
private var shopRepository: ShopRepositoryProtocol
private var itemTagRepository: ItemTagRepositoryProtocol
@State private var isShowingCreateSheet = false
@State private var viewModel: ShopListViewModel

init(
shopRepository: ShopRepositoryProtocol,
itemTagRepository: ItemTagRepositoryProtocol
) {
self.shopRepository = shopRepository
self.itemTagRepository = itemTagRepository
init(viewModel: ShopListViewModel) {
self._viewModel = State(wrappedValue: viewModel)
}
}

extension ShopListView {
var body: some View {
contentView
.task {
reload()
viewModel.reload()
}
.onAppear {
tabViewModel.showingDetailView[mainTab] = false
viewModel.setTabViewModelShowingDetailViewToFalse()
}
.onChange(of: shopRepository.state) {
if shopRepository.state == .initial {
reload()
.onChange(of: viewModel.state) {
if viewModel.state == .initial {
viewModel.reload()
}
}
// Avoid showing deleted shop.
.onChange(of: sessionController.shouldPopToRootView) {
.onChange(of: viewModel.shouldPopToRootView) {
Task {
try await Task.sleep(nanoseconds: 2_000_000_000)
reload()
viewModel.reload()
}
}
}
Expand All @@ -88,7 +79,7 @@ extension ShopListView {
private extension ShopListView {
var contentView: some View {
@ViewBuilder var contentView: some View {
switch shopRepository.state {
switch viewModel.state {
case .initial, .loading:
LoadingView()
case .hasData:
Expand All @@ -101,12 +92,8 @@ private extension ShopListView {
return contentView
}

func reload() {
shopRepository.reload()
}

var cardsView: some View {
ForEach(shopRepository.shops) { shop in
ForEach(viewModel.shops) { shop in
NavigationLink(value: shop) {
ShopListCardView(shop: shop)
}
Expand All @@ -115,11 +102,9 @@ private extension ShopListView {
}

var shopListView: some View {
let leftInShopSlots = shopRepository.limitCount - shopRepository.createdShopsCount

return VStack {
if shopRepository.isEmpty {
noResultsView(leftInShopSlots: leftInShopSlots)
VStack {
if viewModel.isEmpty {
noResultsView(leftInShopSlots: viewModel.leftInShopSlots)
} else {
List {
Section {
Expand All @@ -130,11 +115,11 @@ private extension ShopListView {
.tint(.alarm)

EmptyView()
.id(ScrollToTopID(mainTab: mainTab, detail: false))
.id(viewModel.scrollToTopID())
} footer: {
VStack(spacing: 0) {
HStack(alignment: .firstTextBaseline) {
Text(String(leftInShopSlots))
Text(String(viewModel.leftInShopSlots))
.font(.uiLabelBold)
Text(verbatim: "left in shop slots.")
.font(.uiFootnote)
Expand All @@ -144,41 +129,41 @@ private extension ShopListView {
}
.navigationDestination(for: Shop.self) { shop in
ShopDetailView(
shopRepository: shopRepository,
itemTagRepository: itemTagRepository,
shopRepository: viewModel.shopRepository,
itemTagRepository: viewModel.itemTagRepository,
shopId: shop.id
)
}
.accessibility(identifier: "shopListView")
.refreshable {
reload()
viewModel.reload()
}
}
}
.navigationTitle(String.shops)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if leftInShopSlots > 0 {
if viewModel.leftInShopSlots > 0 {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isShowingCreateSheet.toggle()
viewModel.showCreateView()
} label: {
Image(systemName: "plus")
}
}
}
}
.sheet(isPresented: $isShowingCreateSheet,
.sheet(isPresented: $viewModel.isShowingCreateSheet,
onDismiss: {
reload()
viewModel.reload()
}, content: {
ShopCreateView(shopRepository: shopRepository)
ShopCreateView(shopRepository: viewModel.shopRepository)
}
)
}

var reloadView: some View {
ErrorView(buttonAction: reload)
ErrorView(buttonAction: viewModel.reload)
}

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

MainButtonView(title: String.addShop, type: .primary(withArrow: false)) {
isShowingCreateSheet.toggle()
viewModel.showCreateView()
}
.padding()

Expand Down
59 changes: 59 additions & 0 deletions NativeAppTemplate/UI/Shop List/ShopListViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// ShopListViewModel.swift
// NativeAppTemplate
//
// Created by Claude on 2025/06/22.
//

import SwiftUI
import Observation

@Observable
@MainActor
final class ShopListViewModel {
var isShowingCreateSheet = false

var state: DataState { shopRepository.state }
var shops: [Shop] { shopRepository.shops }
var limitCount: Int { shopRepository.limitCount }
var createdShopsCount: Int { shopRepository.createdShopsCount }
var isEmpty: Bool { shopRepository.isEmpty }
var leftInShopSlots: Int { limitCount - createdShopsCount }
var shouldPopToRootView: Bool { sessionController.shouldPopToRootView }

let shopRepository: ShopRepositoryProtocol
let itemTagRepository: ItemTagRepositoryProtocol
private let sessionController: SessionControllerProtocol
private let tabViewModel: TabViewModel
private let mainTab: MainTab

init(
sessionController: SessionControllerProtocol,
shopRepository: ShopRepositoryProtocol,
itemTagRepository: ItemTagRepositoryProtocol,
tabViewModel: TabViewModel,
mainTab: MainTab
) {
self.sessionController = sessionController
self.shopRepository = shopRepository
self.itemTagRepository = itemTagRepository
self.tabViewModel = tabViewModel
self.mainTab = mainTab
}

func reload() {
shopRepository.reload()
}

func showCreateView() {
isShowingCreateSheet.toggle()
}

func setTabViewModelShowingDetailViewToFalse() {
tabViewModel.showingDetailView[mainTab] = false
}

func scrollToTopID() -> ScrollToTopID {
ScrollToTopID(mainTab: mainTab, detail: false)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// DemoOnboardingRepository.swift
// NativeAppTemplate
//
// Created by Claude on 2025/06/22.
//

@testable import NativeAppTemplate
import Foundation
import OrderedCollections

@MainActor
final class DemoOnboardingRepository: OnboardingRepositoryProtocol {
var onboardings: [Onboarding] = []
var onboardingsDictionary: OrderedDictionary<Int, Bool> {
var dict = OrderedDictionary<Int, Bool>()
for onboarding in onboardings {
dict[onboarding.id] = onboarding.isPortraitImage
}
return dict
}

func reload() {
// Demo data with predefined onboarding items
let demoOnboardingData: OrderedDictionary = [
1: false, // Landscape image
2: false, // Landscape image
3: false, // Landscape image
4: true, // Portrait image
5: false, // Landscape image
6: false, // Landscape image
7: true, // Portrait image
8: true, // Portrait image
9: false, // Landscape image
10: false, // Landscape image
11: true, // Portrait image
12: false, // Landscape image
13: false // Landscape image
]

onboardings = demoOnboardingData.map { key, value in
Onboarding(id: key, isPortraitImage: value)
}
}
}
Loading