Skip to content

Commit 12d0569

Browse files
authored
Merge pull request #63 from Purchasely/feat/searchable-products
feat: searchable and more detailed products list
2 parents 345a350 + 474683c commit 12d0569

File tree

3 files changed

+133
-36
lines changed

3 files changed

+133
-36
lines changed

Example/Packages/Sources/PurchaselyExampleApp/PurchaselySampleV2App.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import SwiftUI
99

1010
@main
1111
public struct PurchaselySampleV2App: App {
12-
public init() { }
12+
public init() {
13+
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).backgroundColor = .white
14+
}
1315

1416
public var body: some Scene {
1517
WindowGroup {

Example/Packages/Sources/PurchaselyExampleApp/Screens/Products/ProductsView.swift

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ class ProductTest {
1515
}
1616

1717
struct ProductsView: View {
18-
19-
@State private var searchText = ""
18+
2019
@StateObject var viewModel = ProductsViewModel()
2120

2221
var body: some View {
@@ -28,11 +27,9 @@ struct ProductsView: View {
2827
.frame(maxHeight: 1)
2928
.navigationBarTitle("Products and Plans", displayMode: .inline)
3029

31-
VStack {
32-
ProductListView()
33-
}
34-
.padding(.top, 16)
35-
.background(Color.white)
30+
ProductListView()
31+
.background(Color.white)
32+
.searchable(text: $viewModel.searchQuery, prompt: "Products and Plans")
3633

3734
}.frame(maxWidth: .infinity,
3835
maxHeight: .infinity,
@@ -42,42 +39,25 @@ struct ProductsView: View {
4239
}
4340

4441
extension ProductsView {
45-
46-
@ViewBuilder
42+
4743
func ProductListView() -> some View {
4844
List {
49-
ForEach(viewModel.plyProducts, id: \.vendorId) { plyProduct in
45+
ForEach(viewModel.searchResults, id: \.vendorId) { plyProduct in
5046
Section(content: {
5147
PlansView(plyProduct)
5248
}, header: {
5349
ProductTitleView(plyProduct)
5450
})
5551
}
56-
}.listRowSpacing(10)
57-
.scrollContentBackground(.hidden)
52+
}
53+
.listRowSpacing(10)
54+
.scrollContentBackground(.hidden)
5855
}
5956

60-
@ViewBuilder
6157
func PlansView(_ plyProduct: SampleProductObject) -> some View {
62-
ForEach(plyProduct.plans) { plan in
63-
VStack(alignment: .leading) {
64-
Text(plan.vendorId)
65-
.bold()
66-
Text(plan.name)
67-
Text(plan.appleProductId)
68-
Button {
69-
UIPasteboard.general.string = plan.vendorId
70-
} label: {
71-
Image(systemName: "doc.on.doc")
72-
.foregroundColor(.white)
73-
.padding(.trailing)
74-
}
75-
}
76-
.listRowBackground(Color.mainLight)
77-
}
58+
ForEach(plyProduct.plans, content: PlanView.init(plan:))
7859
}
7960

80-
@ViewBuilder
8161
func ProductTitleView(_ plyProduct: SampleProductObject) -> some View {
8262
HStack {
8363
VStack(alignment: .leading) {
@@ -87,8 +67,8 @@ extension ProductsView {
8767

8868
Text(plyProduct.vendorId)
8969
.font(.body)
90-
}.padding(.trailing, 16)
91-
70+
}
71+
Spacer()
9272
ZStack {
9373
Capsule()
9474
.frame(width: 80, height: 30)
@@ -102,6 +82,77 @@ extension ProductsView {
10282
}
10383
}
10484

85+
struct PlanView: View {
86+
@State var isExpanded: Bool = false
87+
88+
let plan: SamplePlanObject
89+
90+
init(plan: SamplePlanObject) {
91+
self.plan = plan
92+
}
93+
94+
var body: some View {
95+
VStack(alignment: .leading) {
96+
HStack {
97+
Text(plan.name)
98+
.font(.headline)
99+
Spacer()
100+
Button {
101+
UIPasteboard.general.string = plan.vendorId
102+
} label: {
103+
Image(systemName: "doc.on.doc")
104+
.foregroundColor(.white)
105+
.frame(width: 35, height: 35)
106+
}
107+
.buttonStyle(.plain)
108+
}
109+
HStack {
110+
VStack(alignment: .leading) {
111+
Text(plan.vendorId)
112+
Text(plan.appleProductId)
113+
}
114+
.foregroundStyle(.secondary)
115+
.fontWeight(.semibold)
116+
Spacer()
117+
Button(action: {
118+
isExpanded.toggle()
119+
}, label: {
120+
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
121+
.frame(width: 35, height: 35)
122+
})
123+
.buttonStyle(.plain)
124+
.font(.body)
125+
}
126+
if isExpanded {
127+
Spacer()
128+
.frame(height: 20)
129+
labelWithValue("localized price", with: plan.plyPlan.localizedPrice())
130+
labelWithValue("localized full price", with: plan.plyPlan.localizedFullPrice())
131+
labelWithValue("localized intro. price", with: plan.plyPlan.localizedIntroductoryPrice())
132+
labelWithValue("localized full intro. price", with: plan.plyPlan.localizedFullIntroductoryPrice())
133+
Divider()
134+
labelWithValue("intro. period", with: plan.plyPlan.introductoryPeriod())
135+
labelWithValue("localized period", with: plan.plyPlan.localizedPeriod())
136+
labelWithValue("localized intro. period", with: plan.plyPlan.localizedIntroductoryPeriod())
137+
Divider()
138+
labelWithValue("duration", with: plan.plyPlan.duration)
139+
labelWithValue("intro. duration", with: plan.plyPlan.introductoryDuration())
140+
labelWithValue("localized intro. duration", with: plan.plyPlan.localizedIntroductoryDuration())
141+
Divider()
142+
labelWithValue("amount", with: plan.plyPlan.amount)
143+
labelWithValue("currency code", with: plan.plyPlan.currencyCode)
144+
labelWithValue("currency symbol", with: plan.plyPlan.currencySymbol)
145+
}
146+
}
147+
.font(.subheadline)
148+
.listRowBackground(Color.mainLight)
149+
}
150+
151+
func labelWithValue<T: CustomStringConvertible>(_ string: String, with value: T?) -> Text {
152+
Text("\(string): \(value?.description ?? "nil")")
153+
}
154+
}
155+
105156
#Preview {
106157
ProductsView()
107158
}

Example/Packages/Sources/PurchaselyExampleApp/Screens/Products/ProductsViewModel.swift

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Florian Huet on 22/12/2023.
66
//
77

8+
import Combine
89
import Foundation
910
import Purchasely
1011

@@ -15,6 +16,8 @@ struct SamplePlanObject: Identifiable {
1516
var vendorId: String
1617
var appleProductId: String
1718
var offers: [String]
19+
20+
var plyPlan: PLYPlan
1821
}
1922

2023
struct SampleProductObject: Identifiable, Hashable {
@@ -34,11 +37,22 @@ struct SampleProductObject: Identifiable, Hashable {
3437

3538
class ProductsViewModel: ObservableObject {
3639

37-
@Published var plyProducts: [SampleProductObject] = []
40+
@Published private var plyProducts: [SampleProductObject] = []
3841
@Published var plyPlansStringList: [String] = []
3942
@Published var offersForPlan: [String] = []
40-
43+
@Published var searchQuery: String = ""
44+
45+
@Published var searchResults: [SampleProductObject] = []
46+
4147
init() {
48+
$plyProducts
49+
.combineLatest($searchQuery.map(\.localizedLowercase)) { (products: [SampleProductObject], query: String) -> [SampleProductObject] in
50+
if query.isEmpty {
51+
return products
52+
}
53+
return products.compactMap(match(query: query))
54+
}
55+
.assign(to: &$searchResults)
4256
loadProducts()
4357
}
4458

@@ -50,8 +64,11 @@ class ProductsViewModel: ObservableObject {
5064
SamplePlanObject(name: $0.name ?? "",
5165
vendorId: $0.vendorId,
5266
appleProductId: $0.appleProductId ?? "",
53-
offers: $0.promoOffers.map { $0.vendorId }) })
67+
offers: $0.promoOffers.map { $0.vendorId },
68+
plyPlan: $0) }
69+
.sorted(by: { $0.name < $1.name }))
5470
}
71+
.sorted(by: { $0.name < $1.name })
5572

5673
for product in self.plyProducts {
5774
for plan in product.plans {
@@ -76,3 +93,30 @@ class ProductsViewModel: ObservableObject {
7693
offersForPlan = []
7794
}
7895
}
96+
97+
fileprivate func match(query: String) -> (_ product: SampleProductObject) -> SampleProductObject? {
98+
return { product in
99+
if product.name.lowercased().contains(query) || product.vendorId.lowercased().contains(query) {
100+
return product
101+
}
102+
103+
let filteredPlans = product.plans.filter(match(query: query))
104+
105+
guard !filteredPlans.isEmpty else { return nil }
106+
107+
return SampleProductObject(
108+
id: product.id,
109+
vendorId: product.vendorId,
110+
name: product.name,
111+
plans: filteredPlans
112+
)
113+
}
114+
}
115+
116+
fileprivate func match(query: String) -> (_ plan: SamplePlanObject) -> Bool {
117+
return { plan in
118+
plan.name.contains(query) ||
119+
plan.vendorId.contains(query) ||
120+
plan.appleProductId.contains(query)
121+
}
122+
}

0 commit comments

Comments
 (0)