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
4 changes: 4 additions & 0 deletions NativeAppTemplate.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
017278612D7D83E700CE424F /* ItemTagData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172785D2D7D83E700CE424F /* ItemTagData.swift */; };
017278622D7D83E700CE424F /* ItemTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172785C2D7D83E700CE424F /* ItemTag.swift */; };
017278632D7D83E700CE424F /* ItemTagState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172785F2D7D83E700CE424F /* ItemTagState.swift */; };
4A8DA0DEF6F142C3A127058A /* PaginationMeta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE8A6D1D27C458389C4F61A /* PaginationMeta.swift */; };
017278642D7D83E700CE424F /* ItemTagInfoFromNdefMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172785E2D7D83E700CE424F /* ItemTagInfoFromNdefMessage.swift */; };
017278652D7D83E700CE424F /* ItemTagType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278602D7D83E700CE424F /* ItemTagType.swift */; };
017278682D7D83F600CE424F /* ScanState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017278672D7D83F600CE424F /* ScanState.swift */; };
Expand Down Expand Up @@ -260,6 +261,7 @@
0172785D2D7D83E700CE424F /* ItemTagData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagData.swift; sourceTree = "<group>"; };
0172785E2D7D83E700CE424F /* ItemTagInfoFromNdefMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagInfoFromNdefMessage.swift; sourceTree = "<group>"; };
0172785F2D7D83E700CE424F /* ItemTagState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagState.swift; sourceTree = "<group>"; };
2FE8A6D1D27C458389C4F61A /* PaginationMeta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationMeta.swift; sourceTree = "<group>"; };
017278602D7D83E700CE424F /* ItemTagType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTagType.swift; sourceTree = "<group>"; };
017278672D7D83F600CE424F /* ScanState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanState.swift; sourceTree = "<group>"; };
0172786A2D7D840A00CE424F /* ShowTagInfoScanResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowTagInfoScanResult.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -587,6 +589,7 @@
0172785F2D7D83E700CE424F /* ItemTagState.swift */,
017278602D7D83E700CE424F /* ItemTagType.swift */,
01B526532AF4E36400655131 /* MainTab.swift */,
2FE8A6D1D27C458389C4F61A /* PaginationMeta.swift */,
017278082D7D4F7400CE424F /* Onboarding.swift */,
017278672D7D83F600CE424F /* ScanState.swift */,
01B526552AF4E82A00655131 /* ScrollToTopID.swift */,
Expand Down Expand Up @@ -1053,6 +1056,7 @@
0199CD252E07510200109DC6 /* ItemTagRepositoryProtocol.swift in Sources */,
0199CD262E07510200109DC6 /* ShopRepositoryProtocol.swift in Sources */,
017278632D7D83E700CE424F /* ItemTagState.swift in Sources */,
4A8DA0DEF6F142C3A127058A /* PaginationMeta.swift in Sources */,
017278642D7D83E700CE424F /* ItemTagInfoFromNdefMessage.swift in Sources */,
017278652D7D83E700CE424F /* ItemTagType.swift in Sources */,
0172046625AA82BF008FD63B /* MessageBarView.swift in Sources */,
Expand Down
59 changes: 57 additions & 2 deletions NativeAppTemplate/Data/Repositories/ItemTagRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import SwiftUI

var itemTags: [ItemTag] = []
var state: DataState = .initial
var paginationMeta: PaginationMeta?
var isLoadingMore = false

required init(itemTagsService: ItemTagsService) {
self.itemTagsService = itemTagsService
Expand Down Expand Up @@ -37,7 +39,9 @@ import SwiftUI

Task { @MainActor in
do {
itemTags = try await itemTagsService.allItemTags(shopId: shopId)
let response = try await itemTagsService.allItemTags(shopId: shopId)
itemTags = response.itemTags
paginationMeta = response.paginationMeta
state = .hasData
} catch {
state = .failed
Expand All @@ -48,9 +52,60 @@ import SwiftUI
}
}

func reloadPage(shopId: String, page: Int) {
if Task.isCancelled {
return
}

if state == .loading {
return
}

state = .loading

Task { @MainActor in
do {
let response = try await itemTagsService.allItemTags(shopId: shopId, page: page)
itemTags = response.itemTags
paginationMeta = response.paginationMeta
state = .hasData
} catch {
state = .failed
Failure
.fetch(from: Self.self, reason: error.codedDescription)
.log()
}
}
}

func loadNextPage(shopId: String) {
guard let meta = paginationMeta, meta.hasMorePages else { return }

if isLoadingMore {
return
}

isLoadingMore = true

Task { @MainActor in
do {
let response = try await itemTagsService.allItemTags(shopId: shopId, page: meta.currentPage + 1)
itemTags.append(contentsOf: response.itemTags)
paginationMeta = response.paginationMeta
} catch {
Failure
.fetch(from: Self.self, reason: error.codedDescription)
.log()
}

isLoadingMore = false
}
}

func fetchAll(shopId: String) async throws -> [ItemTag] {
do {
itemTags = try await itemTagsService.allItemTags(shopId: shopId)
let response = try await itemTagsService.allItemTags(shopId: shopId)
itemTags = response.itemTags
return itemTags
} catch {
Failure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import SwiftUI
var itemTags: [ItemTag] { get set }
var state: DataState { get set }
var isEmpty: Bool { get }
var paginationMeta: PaginationMeta? { get }
var isLoadingMore: Bool { get }

init(itemTagsService: ItemTagsService)

func findBy(id: String) -> ItemTag
func reload(shopId: String)
func reloadPage(shopId: String, page: Int)
func loadNextPage(shopId: String)
func fetchAll(shopId: String) async throws -> [ItemTag]
func fetchDetail(id: String) async throws -> ItemTag
func create(shopId: String, itemTag: ItemTag) async throws -> ItemTag
Expand Down
38 changes: 38 additions & 0 deletions NativeAppTemplate/Models/PaginationMeta.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// PaginationMeta.swift
// NativeAppTemplate
//

import Foundation

struct PaginationMeta: Sendable {
let currentPage: Int
let totalPages: Int
let totalCount: Int
let limit: Int

var hasMorePages: Bool {
currentPage < totalPages
}

init(currentPage: Int, totalPages: Int, totalCount: Int, limit: Int) {
self.currentPage = currentPage
self.totalPages = totalPages
self.totalCount = totalCount
self.limit = limit
}

init?(dictionary: [String: Any]) {
guard let currentPage = dictionary["current_page"] as? Int,
let totalPages = dictionary["total_pages"] as? Int,
let totalCount = dictionary["total_count"] as? Int,
let limit = dictionary["limit"] as? Int else {
return nil
}

self.currentPage = currentPage
self.totalPages = totalPages
self.totalCount = totalCount
self.limit = limit
}
}
18 changes: 16 additions & 2 deletions NativeAppTemplate/Networking/Requests/ItemTagsRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@

import Foundation

struct ItemTagsResponse: Sendable {
let itemTags: [ItemTag]
let paginationMeta: PaginationMeta?
}

struct GetItemTagsRequest: Request {
typealias Response = [ItemTag]
typealias Response = ItemTagsResponse

// MARK: - Properties

Expand All @@ -19,18 +24,27 @@ struct GetItemTagsRequest: Request {
}

var additionalHeaders: [String: String] = [:]

var queryItems: [URLQueryItem] {
guard let page else { return [] }
return [URLQueryItem(name: "page", value: String(page))]
}

var body: Data? {
nil
}

let shopId: String
let page: Int?

// MARK: - Internal

func handle(response: Data) throws -> Response {
let json = try JSONSerialization.jsonObject(with: response)
let doc = JSONAPIDocument(json)
return try doc.data.map { try ItemTagAdapter.process(resource: $0) }
let itemTags = try doc.data.map { try ItemTagAdapter.process(resource: $0) }
let paginationMeta = PaginationMeta(dictionary: doc.meta)
return ItemTagsResponse(itemTags: itemTags, paginationMeta: paginationMeta)
}
}

Expand Down
7 changes: 6 additions & 1 deletion NativeAppTemplate/Networking/Requests/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// NativeAppTemplate
//

import struct Foundation.Data
import Foundation

enum HTTPMethod: String {
case GET
Expand All @@ -19,6 +19,7 @@ protocol Request {
var method: HTTPMethod { get }
var path: String { get }
var additionalHeaders: [String: String] { get }
var queryItems: [URLQueryItem] { get }
var body: Data? { get }

func handle(response: Data) throws -> Response
Expand All @@ -30,6 +31,10 @@ extension Request {
.GET
}

var queryItems: [URLQueryItem] {
[]
}

var body: Data? {
nil
}
Expand Down
4 changes: 2 additions & 2 deletions NativeAppTemplate/Networking/Services/ItemTagsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ struct ItemTagsService: Service {
extension ItemTagsService {
// MARK: - Internal

func allItemTags(shopId: String) async throws -> GetItemTagsRequest.Response {
let request = GetItemTagsRequest(shopId: shopId)
func allItemTags(shopId: String, page: Int? = nil) async throws -> GetItemTagsRequest.Response {
let request = GetItemTagsRequest(shopId: shopId, page: page)
return try await makeRequest(request: request)
}

Expand Down
6 changes: 5 additions & 1 deletion NativeAppTemplate/Networking/Services/Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@ extension Service {
pathURL = pathURL.appendingPathComponent(networkClient.environment.basePath)
pathURL = pathURL.appendingPathComponent(request.path)

guard let components = URLComponents(
guard var components = URLComponents(
url: pathURL,
resolvingAgainstBaseURL: false
) else {
throw URLError(.badURL)
}

if !request.queryItems.isEmpty {
components.queryItems = request.queryItems
}

guard let url = components.url
else { throw URLError(.badURL) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,30 @@ private extension ItemTagListView {
if viewModel.isEmpty {
noResultsView
} else {
List(viewModel.itemTags) { itemTag in
NavigationLink(
destination: ItemTagDetailView(
viewModel: viewModel.createItemTagDetailViewModel(itemTagId: itemTag.id)
)
) {
ItemTagListCardView(
itemTag: itemTag
)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) { viewModel.destroyItemTag(itemTagId: itemTag.id) } label: {
Label(String.delete, systemImage: "trash")
.labelStyle(.titleOnly)
List {
ForEach(viewModel.itemTags) { itemTag in
NavigationLink(
destination: ItemTagDetailView(
viewModel: viewModel.createItemTagDetailViewModel(itemTagId: itemTag.id)
)
) {
ItemTagListCardView(
itemTag: itemTag
)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) { viewModel.destroyItemTag(itemTagId: itemTag.id) } label: {
Label(String.delete, systemImage: "trash")
.labelStyle(.titleOnly)
}
.tint(.validationError)
}
.tint(.validationError)
}
.listRowBackground(Color.cardBackground.opacity(0.7))
}

if viewModel.hasMorePages {
loadMoreRow
}
.listRowBackground(Color.cardBackground.opacity(0.7))
}
.refreshable {
viewModel.reload()
Expand Down Expand Up @@ -102,6 +108,19 @@ private extension ItemTagListView {
)
}

var loadMoreRow: some View {
HStack {
Spacer()
ProgressView()
.padding(NativeAppTemplateConstants.Spacing.xxs)
Spacer()
}
.listRowBackground(Color.clear)
.onAppear {
viewModel.loadMore()
}
}

var noResultsView: some View {
VStack {
Image(systemName: "01.square")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ final class ItemTagListViewModel {
itemTagRepository.itemTags
}

var paginationMeta: PaginationMeta? {
itemTagRepository.paginationMeta
}

var isLoadingMore: Bool {
itemTagRepository.isLoadingMore
}

var hasMorePages: Bool {
paginationMeta?.hasMorePages ?? false
}

private let itemTagRepository: ItemTagRepositoryProtocol
private let messageBus: MessageBus
private let sessionController: SessionControllerProtocol
Expand All @@ -46,7 +58,11 @@ final class ItemTagListViewModel {
}

func reload() {
itemTagRepository.reload(shopId: shop.id)
itemTagRepository.reloadPage(shopId: shop.id, page: 1)
}

func loadMore() {
itemTagRepository.loadNextPage(shopId: shop.id)
}

func destroyItemTag(itemTagId: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ final class DemoItemTagRepository: ItemTagRepositoryProtocol {
itemTags.isEmpty
}

var paginationMeta: PaginationMeta?
var isLoadingMore = false

required init(itemTagsService: ItemTagsService) {}

func findBy(id: String) -> ItemTag {
Expand All @@ -30,6 +33,14 @@ final class DemoItemTagRepository: ItemTagRepositoryProtocol {
state = .hasData
}

func reloadPage(shopId: String, page: Int) {
reload(shopId: shopId)
}

func loadNextPage(shopId: String) {
// No-op for demo
}

func fetchAll(shopId: String) async throws -> [ItemTag] {
let allItemTags = fetchAll()
return allItemTags.filter { $0.shopId == shopId }
Expand Down
Loading
Loading