Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Implement UI display with manual scroll for Carousel View (RMCCX-7619) #340

Merged
merged 19 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
71483d9
chore: Implement UI display with manual scroll for Carousel View (RMC…
SoumenRautray Dec 9, 2024
2e02f51
chore: address review comments (RMCCX-7619)
SoumenRautray Dec 9, 2024
1eb1da8
chore: fix sonar cloud issues (RMCCX-7619)
SoumenRautray Dec 9, 2024
7f3b731
chore: refactor code to reduce cognitive complexity (RMCCX-7619)
SoumenRautray Dec 9, 2024
37b8a44
chore: fix UI tests (RMCCX-7619)
SoumenRautray Dec 10, 2024
0ca35a7
chore: fix sonar cloud error and uitest failure (RMCCX-7619)
SoumenRautray Dec 10, 2024
56935ca
fix: bitrise errors (RMCCX-7619)
SoumenRautray Dec 10, 2024
2d87151
chore: change macos version in github workflow
SoumenRautray Dec 11, 2024
65c96fa
chore: fix github issues (RMCCX-7619)
SoumenRautray Dec 11, 2024
91b5bac
chore: refactor code and address review comments (RMCCX-7619)
SoumenRautray Dec 13, 2024
2984c85
chore: fix sonar cloud issues (RMCCX-7619)
SoumenRautray Dec 13, 2024
38ac2f6
chore: add tests (RMCCX-7619)
SoumenRautray Dec 13, 2024
178dc91
chore: add tests (RMCCX-7619)
SoumenRautray Dec 15, 2024
8008963
chore: increase test coverage (RMCCX-7619)
SoumenRautray Dec 17, 2024
792fc43
chore: refactor code (RMCCX-7619)
SoumenRautray Dec 17, 2024
4fddba0
chore: refactor code and fix alignment (RMCCX-7619)
SoumenRautray Dec 18, 2024
7ce15bf
chore: refactor tests for campaign dispatcher (RMCCX-7619)
SoumenRautray Dec 19, 2024
61c08a7
chore: remove deleted file from xcodeproj (RMCCX-7619)
SoumenRautray Dec 19, 2024
ef54bd3
chore: fix bitrise failure (RMCCX-7619)
SoumenRautray Dec 19, 2024
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
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ permissions:

jobs:
build-test:
runs-on: macOS-12
runs-on: macOS-14
steps:
- uses: actions/checkout@v3
with:
Expand All @@ -24,13 +24,13 @@ jobs:
-workspace RInAppMessaging.xcworkspace \
-scheme UITests \
-resultBundlePath artifacts/ui-tests/UITests \
-destination "platform=iOS Simulator,name=iPhone 11" \
-destination "platform=iOS Simulator,name=iPhone 14" \
test | xcpretty
xcodebuild \
-workspace RInAppMessaging.xcworkspace \
-scheme RInAppMessaging-Example \
-resultBundlePath artifacts/unit-tests/RInAppMessaging-Example \
-destination "platform=iOS Simulator,name=iPhone 11" \
-destination "platform=iOS Simulator,name=iPhone 14" \
test | xcpretty
- uses: kishikawakatsumi/xcresulttool@v1
with:
Expand All @@ -40,4 +40,4 @@ jobs:
upload-bundles: false
if: success() || failure()
# ^ This is important because the action will be run
# even if the test fails in the previous step.
# even if the test fails in the previous step.
119 changes: 99 additions & 20 deletions Sources/RInAppMessaging/CampaignDispatcher.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Foundation
import UIKit

internal protocol CampaignDispatcherDelegate: AnyObject {
func performPing()
Expand All @@ -24,6 +24,14 @@ internal class CampaignDispatcher: CampaignDispatcherType, TaskSchedulable {
private let dispatchQueue = DispatchQueue(label: "IAM.CampaignDisplay", qos: .userInteractive)
private(set) var queuedCampaignIDs = [String]()
private(set) var isDispatching = false

private let urlCache: URLCache = {
// response must be <= 5% of mem/disk cap in order to committed to cache
let cache = URLCache(memoryCapacity: URLCache.shared.memoryCapacity,
diskCapacity: 100 * 1024 * 1024, // fits up to 5MB images
diskPath: "RInAppMessaging")
Esakkiraja-Pothikannan marked this conversation as resolved.
Show resolved Hide resolved
return cache
}()

weak var delegate: CampaignDispatcherDelegate?
var scheduledTask: DispatchWorkItem?
Expand All @@ -41,11 +49,7 @@ internal class CampaignDispatcher: CampaignDispatcherType, TaskSchedulable {
sessionConfig.timeoutIntervalForRequest = Constants.CampaignMessage.imageRequestTimeoutSeconds
sessionConfig.timeoutIntervalForResource = Constants.CampaignMessage.imageResourceTimeoutSeconds
sessionConfig.waitsForConnectivity = true
sessionConfig.urlCache = URLCache(
// response must be <= 5% of mem/disk cap in order to committed to cache
memoryCapacity: URLCache.shared.memoryCapacity,
diskCapacity: 100*1024*1024, // fits up to 5MB images
diskPath: "RInAppMessaging")
sessionConfig.urlCache = urlCache
httpSession = URLSession(configuration: sessionConfig)
}

Expand Down Expand Up @@ -100,28 +104,46 @@ internal class CampaignDispatcher: CampaignDispatcherType, TaskSchedulable {
dispatchNext()
return
}
fetchCampaignImagesAndDisplay(campaign: campaign)
}

func fetchCampaignImagesAndDisplay(campaign: Campaign) {
guard let resImgUrlString = campaign.data.messagePayload.resource.imageUrl,
let resImgUrl = URL(string: resImgUrlString) else {
// If no image expected, just display the message.
displayCampaign(campaign)
return
}

// fetch from imageUrl, display if successful, skip on error
if let resImgUrlString = campaign.data.messagePayload.resource.imageUrl, let resImgUrl = URL(string: resImgUrlString) {
data(from: resImgUrl) { imgBlob in
self.dispatchQueue.async {
guard let imgBlob = imgBlob else {
self.dispatchNext()
return
}
self.displayCampaign(campaign, imageBlob: imgBlob)
fetchImage(from: resImgUrl, for: campaign)
}
Esakkiraja-Pothikannan marked this conversation as resolved.
Show resolved Hide resolved

private func fetchImage(from url: URL, for campaign: Campaign) {
data(from: url) { imgBlob in
self.dispatchQueue.async {
guard let imgBlob = imgBlob else {
self.dispatchNext()
return
}
self.handleCampaignImage(campaign, imgBlob: imgBlob)
}
}
}

private func handleCampaignImage(_ campaign: Campaign, imgBlob: Data) {
if let carouselData = campaign.data.customJson?.carousel,
!(carouselData.images?.isEmpty ?? true) {
Esakkiraja-Pothikannan marked this conversation as resolved.
Show resolved Hide resolved
fetchImagesArray(from: carouselData) { images in
self.displayCampaign(campaign, imageBlob: imgBlob, carouselImages: images)
}
Esakkiraja-Pothikannan marked this conversation as resolved.
Show resolved Hide resolved
} else {
// If no image expected, just display the message.
displayCampaign(campaign)
self.displayCampaign(campaign, imageBlob: imgBlob)
}
}

private func displayCampaign(_ campaign: Campaign, imageBlob: Data? = nil) {
private func displayCampaign(_ campaign: Campaign, imageBlob: Data? = nil, carouselImages: [UIImage?]? = nil) {
let campaignTitle = campaign.data.messagePayload.title

router.displayCampaign(campaign, associatedImageData: imageBlob, confirmation: {
router.displayCampaign(campaign, associatedImageData: imageBlob, carouselImages: carouselImages, confirmation: {
let contexts = campaign.contexts
guard let delegate = self.delegate, !contexts.isEmpty, !campaign.data.isTest else {
return true
Expand Down Expand Up @@ -167,4 +189,61 @@ internal class CampaignDispatcher: CampaignDispatcherType, TaskSchedulable {
completion(data)
}.resume()
}

func fetchImagesArray(from carousel: Carousel, completion: @escaping ([UIImage?]) -> Void) {
guard let imageDetails = carousel.images else {
completion([])
return
}

let filteredDetails = imageDetails
.sorted { $0.key < $1.key }
.prefix(Constants.CampaignMessage.carouselThreshold)
.map { $0 }
Esakkiraja-Pothikannan marked this conversation as resolved.
Show resolved Hide resolved

var images: [UIImage?] = Array(repeating: nil, count: filteredDetails.count)

for (index, detail) in filteredDetails.enumerated() {
guard let urlString = detail.value.imgUrl else {
images[index] = nil
continue
}
fetchCarouselImage(for: urlString) { image in
images[index] = image
}
}
completion(images)
}
Esakkiraja-Pothikannan marked this conversation as resolved.
Show resolved Hide resolved

func fetchCarouselImage(for urlString: String, completion: @escaping (UIImage?) -> Void) {
guard let url = URL(string: urlString),
["jpg", "jpeg", "png"].contains(url.pathExtension.lowercased()) else {
completion(nil)
return
}

let request = URLRequest(url: url)

// Check cache
if let cachedResponse = self.urlCache.cachedResponse(for: request),
let cachedImage = UIImage(data: cachedResponse.data) {
completion(cachedImage)
return
}

URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
Esakkiraja-Pothikannan marked this conversation as resolved.
Show resolved Hide resolved
let response = response,
error == nil,
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let image = UIImage(data: data) {
let cachedData = CachedURLResponse(response: response, data: data)
self.urlCache.storeCachedResponse(cachedData, for: request)
completion(image)
} else {
completion(nil)
}
}.resume()
}
}
1 change: 1 addition & 0 deletions Sources/RInAppMessaging/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ internal enum Constants {
enum CampaignMessage {
static let imageRequestTimeoutSeconds: TimeInterval = 20
static let imageResourceTimeoutSeconds: TimeInterval = 300
static let carouselThreshold: Int = 5
Esakkiraja-Pothikannan marked this conversation as resolved.
Show resolved Hide resolved
}

enum Request {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ struct Carousel: Codable {
case images
}

init(images: [String: ImageDetails]?) {
self.images = images
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
images = try container.decodeIfPresent([String: ImageDetails].self, forKey: .images)
Expand All @@ -177,6 +181,12 @@ struct ImageDetails: Codable {
case altText
}

init(imgUrl: String?, link: String?, altText: String?) {
self.imgUrl = imgUrl
self.altText = altText
self.link = link
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
imgUrl = try container.decodeIfPresent(String.self, forKey: .imgUrl)
Expand Down
Loading
Loading