Skip to content

Commit bda64cc

Browse files
authored
Updates to In-App Components (#10)
* update swipe actions and tenantId * Added theme for FilterBar * cleanup * Update version
1 parent 2f5280f commit bda64cc

14 files changed

+183
-91
lines changed

Knock.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |spec|
22
spec.name = "Knock"
3-
spec.version = "1.2.0"
3+
spec.version = "1.2.1"
44
spec.summary = "An SDK to build in-app notifications experiences in Swift with Knock.."
55

66
spec.description = <<-DESC

Sources/Components/Feed/FeedNotificationRow.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import SwiftUI
99
import WebKit
10-
import UIKit
1110

1211

1312
extension Knock {

Sources/Components/Feed/InAppFeedView.swift

Lines changed: 33 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,7 @@ extension Knock {
2020

2121
public var body: some View {
2222
VStack(alignment: .leading, spacing: .zero) {
23-
VStack(alignment: .leading, spacing: .zero) {
24-
if let title = theme.titleString {
25-
Text(title)
26-
.font(theme.titleFont)
27-
.foregroundStyle(theme.titleColor)
28-
.padding(.horizontal, 24)
29-
}
30-
31-
if viewModel.filterOptions.count > 1 {
32-
filterTabView()
33-
.padding(.bottom, 12)
34-
}
35-
36-
if let topButtons = viewModel.topButtonActions {
37-
topActionButtonsView(topButtons: topButtons)
38-
.padding(.bottom, 12)
39-
Divider()
40-
}
41-
}
42-
.background(theme.upperBackgroundColor)
23+
topSectionView()
4324

4425
ZStack(alignment: .bottom) {
4526
Group {
@@ -78,16 +59,18 @@ extension Knock {
7859
viewModel.feedItemRowTapped(item: item)
7960
}
8061
.swipeActions(edge: .trailing) {
81-
if let config = theme.rowTheme.swipeLeftConfig {
82-
Knock.SwipeButton(config: config) {
83-
viewModel.didSwipeRow(item: item, swipeAction: config.action)
62+
if let config = theme.rowTheme.archiveSwipeConfig {
63+
let useInverse = item.archived_at != nil
64+
Knock.SwipeButton(config: config, useInverse: useInverse) {
65+
viewModel.didSwipeRow(item: item, swipeAction: config.action, useInverse: useInverse)
8466
}
8567
}
8668
}
8769
.swipeActions(edge: .leading) {
88-
if let config = theme.rowTheme.swipeRightConfig {
89-
Knock.SwipeButton(config: config) {
90-
viewModel.didSwipeRow(item: item, swipeAction: config.action)
70+
if let config = theme.rowTheme.markAsReadSwipeConfig {
71+
let useInverse = item.read_at != nil
72+
Knock.SwipeButton(config: config, useInverse: useInverse) {
73+
viewModel.didSwipeRow(item: item, swipeAction: config.action, useInverse: useInverse)
9174
}
9275
}
9376
}
@@ -127,6 +110,30 @@ extension Knock {
127110
}
128111
}
129112

113+
@ViewBuilder
114+
private func topSectionView() -> some View {
115+
VStack(alignment: .leading, spacing: .zero) {
116+
if let title = theme.titleString {
117+
Text(title)
118+
.font(theme.titleFont)
119+
.foregroundStyle(theme.titleColor)
120+
.padding(.horizontal, 24)
121+
}
122+
123+
if viewModel.filterOptions.count > 1 {
124+
FilterBarView(filters: viewModel.filterOptions, selectedFilter: $viewModel.currentFilter)
125+
.padding(.bottom, 12)
126+
}
127+
128+
if let topButtons = viewModel.topButtonActions {
129+
topActionButtonsView(topButtons: topButtons)
130+
.padding(.bottom, 12)
131+
Divider()
132+
}
133+
}
134+
.background(theme.upperBackgroundColor)
135+
}
136+
130137
@ViewBuilder
131138
private func lastRowView() -> some View {
132139
HStack {
@@ -144,40 +151,6 @@ extension Knock {
144151
}
145152
}
146153

147-
@ViewBuilder
148-
private func filterTabView() -> some View {
149-
ZStack(alignment: .bottom) {
150-
Divider()
151-
.frame(height: 1)
152-
.background(KnockColor.Gray.gray4)
153-
154-
HStack(spacing: .zero
155-
) {
156-
ForEach(viewModel.filterOptions, id: \.self) { option in
157-
Text(option.title)
158-
.font(.knock2.weight(.medium))
159-
.foregroundColor(option == viewModel.currentFilter ? KnockColor.Accent.accent11 : KnockColor.Gray.gray11)
160-
.padding(.vertical, 10)
161-
.padding(.horizontal, 16)
162-
.overlay(
163-
Rectangle()
164-
.frame(height: 1)
165-
.foregroundColor(option == viewModel.currentFilter ? KnockColor.Accent.accent9 : .clear),
166-
alignment: .bottom
167-
)
168-
.onTapGesture {
169-
withAnimation {
170-
viewModel.currentFilter = option
171-
}
172-
}
173-
}
174-
Spacer()
175-
}
176-
.padding(.horizontal, 24)
177-
}
178-
179-
}
180-
181154
@ViewBuilder
182155
private func topActionButtonsView(topButtons: [Knock.FeedTopActionButtonType]) -> some View {
183156
HStack(alignment: .center, spacing: 12) {

Sources/Components/Feed/InAppFeedViewModel.swift

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import Combine
1111
extension Knock {
1212
public class InAppFeedViewModel: ObservableObject {
1313
@Published public var feed: Knock.Feed = Knock.Feed() /// The current feed data.
14-
@Published public var currentTenantId: String? /// The tenant ID associated with the current feed.
1514
@Published public var filterOptions: [InAppFeedFilter] /// Available filter options for the feed.
1615
@Published public var topButtonActions: [Knock.FeedTopActionButtonType]? /// Actions available at the top of the feed interface.
1716
@Published internal var brandingRequired: Bool = true
@@ -23,8 +22,8 @@ extension Knock {
2322
}
2423

2524
public var feedClientOptions: Knock.FeedClientOptions /// Configuration options for feed.
26-
public let didTapFeedItemButtonPublisher = PassthroughSubject<String, Never>() /// Publisher for feed item button tap events.
27-
public let didTapFeedItemRowPublisher = PassthroughSubject<Knock.FeedItem, Never>() /// Publisher for feed item row tap events.
25+
public var didTapFeedItemButtonPublisher = PassthroughSubject<String, Never>() /// Publisher for feed item button tap events.
26+
public var didTapFeedItemRowPublisher = PassthroughSubject<Knock.FeedItem, Never>() /// Publisher for feed item row tap events.
2827

2928
public var shouldHideArchived: Bool {
3029
(feedClientOptions.archived == .exclude || feedClientOptions.archived == nil)
@@ -36,19 +35,15 @@ extension Knock {
3635

3736
public init(
3837
feedClientOptions: Knock.FeedClientOptions = .init(),
39-
currentTenantId: String? = nil,
4038
currentFilter: InAppFeedFilter? = nil,
4139
filterOptions: [InAppFeedFilter]? = nil,
4240
topButtonActions: [Knock.FeedTopActionButtonType]? = [.markAllAsRead(), .archiveRead()]
4341
) {
4442
self.feedClientOptions = feedClientOptions
45-
self.currentTenantId = currentTenantId ?? feedClientOptions.tenant
4643
self.filterOptions = filterOptions ?? [.init(scope: .all), .init(scope: .unread), .init(scope: .archived)]
4744
self.currentFilter = currentFilter ?? filterOptions?.first ?? .init(scope: .all)
4845
self.topButtonActions = topButtonActions
49-
5046
self.feedClientOptions.status = self.currentFilter.scope
51-
self.feedClientOptions.tenant = self.currentTenantId
5247
}
5348

5449
// MARK: Public Methods
@@ -129,7 +124,7 @@ extension Knock {
129124
default: break
130125
}
131126

132-
let feedOptions = Knock.FeedClientOptions(status: archivedScope, tenant: currentTenantId, has_tenant: feedClientOptions.has_tenant, archived: feedClientOptions.archived)
127+
let feedOptions = Knock.FeedClientOptions(status: archivedScope, tenant: feedClientOptions.tenant, has_tenant: feedClientOptions.has_tenant, archived: feedClientOptions.archived)
133128
do {
134129
_ = try await Knock.shared.feedManager?.makeBulkStatusUpdate(type: updatedStatus, options: feedOptions)
135130
await optimisticallyBulkUpdateStatus(updatedStatus: updatedStatus, archivedScope: archivedScope)
@@ -142,7 +137,7 @@ extension Knock {
142137
switch updatedStatus {
143138
case .seen: guard item.seen_at == nil else { return }
144139
case .read: guard item.read_at == nil else { return }
145-
case .interacted: guard item.inserted_at == nil else { return }
140+
case .interacted: guard item.interacted_at == nil else { return }
146141
case .archived: guard item.archived_at == nil else { return }
147142
case .unread: guard item.read_at != nil else { return }
148143
case .unseen: guard item.seen_at != nil else { return }
@@ -173,12 +168,11 @@ extension Knock {
173168

174169
// MARK: Button/Swipe Interactions
175170

176-
public func didSwipeRow(item: Knock.FeedItem, swipeAction: FeedNotificationRowSwipeAction) {
171+
public func didSwipeRow(item: Knock.FeedItem, swipeAction: FeedNotificationRowSwipeAction, useInverse: Bool) {
177172
Task {
178173
switch swipeAction {
179-
case .archive: await updateMessageEngagementStatus(item, updatedStatus: .archived)
180-
case .markAsRead: await updateMessageEngagementStatus(item, updatedStatus: .read)
181-
case .markAsUnread: await updateMessageEngagementStatus(item, updatedStatus: .unread)
174+
case .archive: await updateMessageEngagementStatus(item, updatedStatus: useInverse ? .unarchived : .archived)
175+
case .markAsRead: await updateMessageEngagementStatus(item, updatedStatus: useInverse ? .unread : .read)
182176
}
183177
}
184178
}
@@ -249,7 +243,7 @@ extension Knock {
249243

250244
private func shouldArchive(item: Knock.FeedItem, scope: Knock.FeedItemScope) -> Bool {
251245
switch scope {
252-
case .interacted: return item.inserted_at != nil
246+
case .interacted: return item.interacted_at != nil
253247
case .unread: return item.read_at == nil
254248
case .read: return item.read_at != nil
255249
case .unseen: return item.seen_at == nil

Sources/Components/Feed/Models/FeedNotificationRowSwipeAction.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,39 @@ extension Knock {
1212
public enum FeedNotificationRowSwipeAction {
1313
case archive
1414
case markAsRead
15-
case markAsUnread
1615

1716
public var defaultTitle: String {
1817
switch self {
1918
case .archive: return "Archive"
2019
case .markAsRead: return "Read"
21-
case .markAsUnread: return "Unread"
20+
}
21+
}
22+
23+
public var defaultInverseTitle: String {
24+
switch self {
25+
case .archive: return "Unarchive"
26+
case .markAsRead: return "Unread"
2227
}
2328
}
2429

2530
public var defaultImage: Image {
2631
switch self {
2732
case .archive: return Image(systemName: "archivebox")
2833
case .markAsRead: return Image(systemName: "envelope.open")
29-
case .markAsUnread: return Image(systemName: "envelope")
34+
}
35+
}
36+
37+
public var defaultInverseImage: Image {
38+
switch self {
39+
case .archive: return Image(systemName: "archivebox")
40+
case .markAsRead: return Image(systemName: "envelope")
3041
}
3142
}
3243

3344
public var defaultSwipeColor: Color {
3445
switch self {
3546
case .archive: return KnockColor.Green.green9
3647
case .markAsRead: return KnockColor.Blue.blue9
37-
case .markAsUnread: return KnockColor.Blue.blue9
3848
}
3949
}
4050

Sources/Components/Feed/Support/ActionButton.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ extension Knock {
1717
Button(action: action) {
1818
Text(title)
1919
.font(config.font)
20+
.lineLimit(1)
2021
.foregroundStyle(config.textColor)
2122
.padding(.vertical, 8)
2223
.frame(maxWidth: config.fillAvailableSpace ? .infinity : .none)
@@ -81,4 +82,5 @@ extension Knock {
8182
Knock.ActionButton(title: "Secondary", config: Knock.ActionButton.Style.secondary.defaultConfig) {}
8283
Knock.ActionButton(title: "Tertiary", config: Knock.ActionButton.Style.tertiary.defaultConfig) {}
8384
}
85+
.padding()
8486
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// FilterBarView.swift
3+
//
4+
//
5+
// Created by Matt Gardner on 6/24/24.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
11+
extension Knock {
12+
struct FilterBarView: View {
13+
var filters: [InAppFeedFilter]
14+
@Binding var selectedFilter: InAppFeedFilter
15+
let theme: FilterBarTheme = FilterBarTheme()
16+
17+
var body: some View {
18+
ZStack(alignment: .bottom) {
19+
Divider()
20+
.frame(height: 1)
21+
.background(KnockColor.Gray.gray4)
22+
23+
HStack(spacing: .zero
24+
) {
25+
ForEach(filters, id: \.self) { option in
26+
Text(option.title)
27+
.font(theme.font)
28+
.foregroundColor(option == selectedFilter ? theme.selectedColor : theme.unselectedColor)
29+
.padding(.vertical, 10)
30+
.padding(.horizontal, 16)
31+
.overlay(
32+
Rectangle()
33+
.frame(height: 1)
34+
.foregroundColor(option == selectedFilter ? theme.selectedColor : .clear),
35+
alignment: .bottom
36+
)
37+
.onTapGesture {
38+
withAnimation {
39+
selectedFilter = option
40+
}
41+
}
42+
}
43+
Spacer()
44+
}
45+
.padding(.horizontal, 24)
46+
}
47+
}
48+
}
49+
}

Sources/Components/Feed/Support/SwipeButton.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@ import SwiftUI
1010
extension Knock {
1111
struct SwipeButton: View {
1212
let config: SwipeButtonConfig
13+
let useInverse: Bool
1314
let action: () -> Void
15+
1416
var body: some View {
1517
Button(action: action) {
1618
VStack(alignment: .center, spacing: 10) {
17-
config.image
18-
Text(config.title)
19+
if useInverse {
20+
config.inverseImage
21+
} else {
22+
config.image
23+
}
24+
Text(useInverse ? config.inverseTitle : config.title)
1925
.font(config.titleFont)
2026
.foregroundStyle(config.titleColor)
2127
}
@@ -27,26 +33,32 @@ extension Knock {
2733
public struct SwipeButtonConfig {
2834
public var action: Knock.FeedNotificationRowSwipeAction
2935
public var title: String
36+
public var inverseTitle: String
3037
public var titleFont: Font
3138
public var titleColor: Color
3239
public var image: Image
40+
public var inverseImage: Image
3341
public var swipeColor: Color
3442
public var showIcon: Bool
3543

3644
public init(
3745
action: Knock.FeedNotificationRowSwipeAction,
3846
title: String? = nil,
47+
inverseTitle: String? = nil,
3948
titleFont: Font? = nil,
4049
titleColor: Color? = nil,
4150
image: Image? = nil,
51+
inverseImage: Image? = nil,
4252
swipeColor: Color? = nil,
4353
showIcon: Bool = true
4454
) {
4555
self.action = action
4656
self.title = title ?? action.defaultTitle
57+
self.inverseTitle = inverseTitle ?? action.defaultInverseTitle
4758
self.titleFont = titleFont ?? .knock2.weight(.medium)
4859
self.titleColor = titleColor ?? .white
4960
self.image = image ?? action.defaultImage
61+
self.inverseImage = inverseImage ?? action.defaultInverseImage
5062
self.swipeColor = swipeColor ?? action.defaultSwipeColor
5163
self.showIcon = showIcon
5264
}

0 commit comments

Comments
 (0)