[Framework] Feed List improvements #19

Merged
javier merged 8 commits from framework/feed/view-model-tests into main 2024-03-22 14:44:52 +00:00
6 changed files with 919 additions and 102 deletions

View File

@ -0,0 +1,65 @@
//
// Array+ReviewDTOs.swift
// ReviewsFeed
//
// Created by Javier Cicchelli on 22/03/2024.
// Copyright © 2024 Röck+Cöde. All rights reserved.
//
import Foundation
import ReviewsFeedKit
extension Array where Element == ReviewsFeedKit.Review {
// MARK: Constants
static let none: [ReviewsFeedKit.Review] = []
static let sample: [ReviewsFeedKit.Review] = [
.init(
id: 1,
author: "Some author name #1 here",
title: "Some review title #1 goes here...",
content: "Some long, long, explanatory review comment #1 goes here...",
rating: 3,
version: "v1.0.0",
updated: .init()
),
.init(
id: 2,
author: "Some author name #2 here",
title: "Some review title #2 goes here...",
content: "Some long, long, explanatory review comment #2 goes here...",
rating: 5,
version: "v1.0.0",
updated: .init()
),
.init(
id: 3,
author: "Some author name #3 here",
title: "Some review title #3 goes here...",
content: "Some long, long, explanatory review comment #3 goes here...",
rating: 1,
version: "v1.0.0",
updated: .init()
),
.init(
id: 4,
author: "Some author name #4 here",
title: "Some review title #4 goes here...",
content: "Some long, long, explanatory review comment #4 goes here...",
rating: 4,
version: "v1.0.0",
updated: .init()
),
.init(
id: 5,
author: "Some author name #5 here",
title: "Some review title #5 goes here...",
content: "Some long, long, explanatory review comment #5 goes here...",
rating: 2,
version: "v1.0.0",
updated: .init()
),
]
}

View File

@ -20,14 +20,14 @@ extension FeedListViewController {
private let topWords: TopWordsUseCase = .init() private let topWords: TopWordsUseCase = .init()
// MARK: Properties // MARK: Properties
@Published var filter: FilterOption = .all @Published private(set) var filter: FilterOption = .all
@Published var isFilterEnabled: Bool = false @Published private(set) var isFilterEnabled: Bool = false
@Published var isFiltering: Bool = false @Published private(set) var isFiltering: Bool = false
@Published var isLoading: Bool = false @Published private(set) var isLoading: Bool = false
@Published var state: FeedListState = .initial @Published private(set) var state: FeedListState = .initial
var items: [Review] = [] private(set) var items: [Review] = []
var words: [TopWord] = [] private(set) var words: [TopWord] = []
private var reviewsAll: [Review] = [] private var reviewsAll: [Review] = []
private var reviewsFiltered: FilteredReviews = [:] private var reviewsFiltered: FilteredReviews = [:]
@ -57,50 +57,51 @@ extension FeedListViewController {
var isWordsShowing: Bool { var isWordsShowing: Bool {
filter != .all filter != .all
&& !words.isEmpty && !words.isEmpty
} }
// MARK: Functions // MARK: Functions
func fetch() { func fetch() async {
Task { isFilterEnabled = false
isFilterEnabled = false isLoading = items.isEmpty
isLoading = items.isEmpty
do { do {
let output = try await iTunesService.getReviews(.init( let output = try await iTunesService.getReviews(.init(
appID: configuration.appID, appID: configuration.appID,
countryCode: configuration.countryCode countryCode: configuration.countryCode
)) ))
reviewsAll = output.reviews.map(Review.init) reviewsAll = output.reviews.map(Review.init)
reviewsFiltered = FilterOption.allCases reviewsFiltered = FilterOption.allCases
.reduce(into: FilteredReviews()) { partialResult, option in .reduce(into: FilteredReviews()) { partialResult, option in
partialResult[option] = reviewsAll.filter { $0.rating.stars == option.rawValue } partialResult[option] = reviewsAll.filter { $0.rating.stars == option.rawValue }
} }
reviewsTopWords = reviewsFiltered reviewsTopWords = reviewsFiltered
.mapValues { reviews in .mapValues { reviews in
reviews.map(\.comment) reviews.map(\.comment)
.compactMap { try? filterWords($0) } .compactMap { try? filterWords($0) }
} }
.mapValues { .mapValues {
topWords($0).map(TopWord.init) topWords($0).map(TopWord.init)
} }
items = filter == .all items = filter == .all
? reviewsAll ? reviewsAll
: reviewsFiltered[filter] ?? [] : reviewsFiltered[filter] ?? []
words = filter == .all
? []
: reviewsTopWords[filter] ?? []
isFilterEnabled = !items.isEmpty isFilterEnabled = !items.isEmpty
state = items.isEmpty state = items.isEmpty
? .empty ? .empty
: .populated : .populated
} catch { } catch {
items = [] items = []
state = .error state = .error
}
isLoading = false
} }
isLoading = false
} }
func filter(by option: FilterOption) { func filter(by option: FilterOption) {
@ -115,16 +116,19 @@ extension FeedListViewController {
} }
func item(for index: Int) -> Review? { func item(for index: Int) -> Review? {
guard guard !items.isEmpty else {
!items.isEmpty,
index < items.count
else {
return nil return nil
} }
return isWordsShowing let indexToUse = isWordsShowing
? items[index - 1] ? index - 1
: items[index] : index
guard indexToUse < items.count else {
return nil
}
return items[indexToUse]
} }
func openItem(at index: Int) { func openItem(at index: Int) {

View File

@ -118,7 +118,7 @@ final class FeedListViewController: UIViewController {
registerTableCells() registerTableCells()
bindViewModel() bindViewModel()
viewModel.fetch() Task { await viewModel.fetch() }
} }
} }
@ -170,7 +170,7 @@ private extension FeedListViewController {
// MARK: Actions // MARK: Actions
@objc func refresh(_ sender: AnyObject) { @objc func refresh(_ sender: AnyObject) {
self.viewModel.fetch() Task { await self.viewModel.fetch() }
} }
// MARK: Functions // MARK: Functions
@ -264,7 +264,7 @@ private extension FeedListViewController {
) )
: nil, : nil,
action: isErrorState action: isErrorState
? { self.viewModel.fetch() } ? { Task { await self.viewModel.fetch() } }
: nil : nil
) )
@ -455,53 +455,7 @@ import ReviewsiTunesKit
#Preview("Feed List with few reviews") { #Preview("Feed List with few reviews") {
MockURLProtocol.response = .init( MockURLProtocol.response = .init(
statusCode: 200, statusCode: 200,
object: Feed(entries: [ object: Feed(entries: .sample)
.init(
id: 1,
author: "Some author name #1 here",
title: "Some review title #1 goes here...",
content: "Some long, explanatory review comment #1 goes here...",
rating: 3,
version: "v1.0.0",
updated: .init()
),
.init(
id: 2,
author: "Some author name #2 here",
title: "Some review title #2 goes here...",
content: "Some long, explanatory review comment #2 goes here...",
rating: 5,
version: "v1.0.0",
updated: .init()
),
.init(
id: 3,
author: "Some author name #3 here",
title: "Some review title #3 goes here...",
content: "Some long, explanatory review comment #3 goes here...",
rating: 1,
version: "v1.0.0",
updated: .init()
),
.init(
id: 4,
author: "Some author name #4 here",
title: "Some review title #4 goes here...",
content: "Some long, explanatory review comment #4 goes here...",
rating: 4,
version: "v1.0.0",
updated: .init()
),
.init(
id: 5,
author: "Some author name #5 here",
title: "Some review title #5 goes here...",
content: "Some long, explanatory review comment #5 goes here...",
rating: 2,
version: "v1.0.0",
updated: .init()
),
])
) )
return UINavigationController(rootViewController: FeedListViewController(.init( return UINavigationController(rootViewController: FeedListViewController(.init(

View File

@ -0,0 +1,21 @@
//
// FeedListCoordinationSpy.swift
// ReviewsFeedTest
//
// Created by Javier Cicchelli on 22/03/2024.
// Copyright © 2024 Röck+Cöde. All rights reserved.
//
@testable import ReviewsFeed
final class FeedListCoordinationSpy: FeedListCoordination {
// MARK: Properties
var itemOpened: Bool = false
// MARK: Functions
func open(_ item: ReviewsFeed.Review) {
itemOpened = true
}
}

View File

@ -0,0 +1,581 @@
//
// FeedListViewModelTests.swift
// ReviewsFeedTests
//
// Created by Javier Cicchelli on 22/03/2024.
// Copyright © 2024 Röck+Cöde. All rights reserved.
//
import Combine
import ReviewsFoundationKit
import ReviewsiTunesKit
import XCTest
@testable import ReviewsFeed
final class FeedListViewModelTests: XCTestCase {
// MARK: Constants
private let coordination: FeedListCoordinationSpy = .init()
// MARK: Properties
private var sut: FeedListViewController.ViewModel!
private var cancellables: Set<AnyCancellable> = []
// MARK: Setup
override func setUp() async throws {
sut = .init(
configuration: .init(session: .mock),
coordination: coordination
)
}
override func tearDown() async throws {
cancellables.removeAll()
}
// MARK: Initialisers tests
func testInit() {
// GIVEN
// WHEN
// THEN
XCTAssertEqual(sut.filter, .all)
XCTAssertFalse(sut.isFilterEnabled)
XCTAssertFalse(sut.isFiltering)
XCTAssertFalse(sut.isLoading)
XCTAssertFalse(sut.isWordsShowing)
XCTAssertTrue(sut.items.isEmpty)
XCTAssertEqual(sut.itemsCount, 0)
XCTAssertEqual(sut.state, .initial)
XCTAssertTrue(sut.words.isEmpty)
}
// MARK: Functions tests
func testFetch_allItems_whenResponseOK_withSomeItems() async {
let expectation = XCTestExpectation()
var isLoading: [Bool] = []
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
sut.$isLoading
.collect(3)
.sink { value in
isLoading.append(contentsOf: value)
expectation.fulfill()
}
.store(in: &cancellables)
await sut.fetch()
wait(for: [expectation], timeout: 2)
// THEN
XCTAssertEqual(isLoading, [false, true, false])
XCTAssertTrue(sut.isFilterEnabled)
XCTAssertFalse(sut.isWordsShowing)
XCTAssertFalse(sut.items.isEmpty)
XCTAssertEqual(sut.items.count, 5)
XCTAssertEqual(sut.itemsCount, 5)
XCTAssertEqual(sut.state, .populated)
}
func testFetch_allItems_whenResponseOK_withNoItems() async {
let expectation = XCTestExpectation()
var isLoading: [Bool] = []
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .none)
)
// WHEN
sut.$isLoading
.collect(3)
.sink { value in
isLoading.append(contentsOf: value)
expectation.fulfill()
}
.store(in: &cancellables)
await sut.fetch()
wait(for: [expectation], timeout: 2)
// THEN
XCTAssertEqual(isLoading, [false, true, false])
XCTAssertFalse(sut.isFilterEnabled)
XCTAssertFalse(sut.isWordsShowing)
XCTAssertTrue(sut.items.isEmpty)
XCTAssertEqual(sut.itemsCount, 0)
XCTAssertEqual(sut.state, .empty)
}
func testFetch_allItems_whenResponseNotOK() async {
let expectation = XCTestExpectation()
var isLoading: [Bool] = []
// GIVEN
MockURLProtocol.response = .init(statusCode: 404)
// WHEN
sut.$isLoading
.collect(3)
.sink { value in
isLoading.append(contentsOf: value)
expectation.fulfill()
}
.store(in: &cancellables)
await sut.fetch()
wait(for: [expectation], timeout: 2)
// THEN
XCTAssertEqual(isLoading, [false, true, false])
XCTAssertFalse(sut.isFilterEnabled)
XCTAssertFalse(sut.isWordsShowing)
XCTAssertTrue(sut.items.isEmpty)
XCTAssertEqual(sut.itemsCount, 0)
XCTAssertEqual(sut.state, .error)
}
func testFetch_filteredItems_whenResponseOK_withSomeItems() async {
let expectation = XCTestExpectation()
var isLoading: [Bool] = []
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
sut.$isLoading
.collect(3)
.sink { value in
isLoading.append(contentsOf: value)
expectation.fulfill()
}
.store(in: &cancellables)
sut.filter(by: .only1Star)
await sut.fetch()
wait(for: [expectation], timeout: 2)
// THEN
XCTAssertEqual(isLoading, [false, true, false])
XCTAssertEqual(sut.filter, .only1Star)
XCTAssertTrue(sut.isFilterEnabled)
XCTAssertTrue(sut.isWordsShowing)
XCTAssertFalse(sut.items.isEmpty)
XCTAssertEqual(sut.items.count, 1)
XCTAssertEqual(sut.itemsCount, 2)
XCTAssertEqual(sut.state, .populated)
XCTAssertFalse(sut.words.isEmpty)
XCTAssertEqual(sut.words.count, 3)
}
func testFetch_filteredItems_whenResponseOK_withNoItems() async {
let expectation = XCTestExpectation()
var isLoading: [Bool] = []
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .none)
)
// WHEN
sut.$isLoading
.collect(3)
.sink { value in
isLoading.append(contentsOf: value)
expectation.fulfill()
}
.store(in: &cancellables)
sut.filter(by: .only1Star)
await sut.fetch()
wait(for: [expectation], timeout: 2)
// THEN
XCTAssertEqual(isLoading, [false, true, false])
XCTAssertEqual(sut.filter, .only1Star)
XCTAssertFalse(sut.isFilterEnabled)
XCTAssertFalse(sut.isWordsShowing)
XCTAssertTrue(sut.items.isEmpty)
XCTAssertEqual(sut.itemsCount, 0)
XCTAssertEqual(sut.state, .empty)
XCTAssertTrue(sut.words.isEmpty)
}
func testFetch_filteredItems_whenResponseNotOK() async {
let expectation = XCTestExpectation()
var isLoading: [Bool] = []
// GIVEN
MockURLProtocol.response = .init(statusCode: 404)
// WHEN
sut.$isLoading
.collect(3)
.sink { value in
isLoading.append(contentsOf: value)
expectation.fulfill()
}
.store(in: &cancellables)
sut.filter(by: .only1Star)
await sut.fetch()
wait(for: [expectation], timeout: 2)
// THEN
XCTAssertEqual(isLoading, [false, true, false])
XCTAssertEqual(sut.filter, .only1Star)
XCTAssertFalse(sut.isFilterEnabled)
XCTAssertFalse(sut.isWordsShowing)
XCTAssertTrue(sut.items.isEmpty)
XCTAssertEqual(sut.itemsCount, 0)
XCTAssertEqual(sut.state, .error)
XCTAssertTrue(sut.words.isEmpty)
}
func testFilter_forNewOption_withSomeItems() async {
let expectation = XCTestExpectation()
var filter: [FilterOption] = []
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
sut.$filter
.dropFirst()
.sink { value in
filter.append(value)
expectation.fulfill()
}
.store(in: &cancellables)
await sut.fetch()
sut.filter(by: .only1Star)
wait(for: [expectation], timeout: 2)
// THEN
XCTAssertEqual(filter, [.only1Star])
XCTAssertFalse(sut.items.isEmpty)
XCTAssertEqual(sut.items.count, 1)
XCTAssertEqual(sut.itemsCount, 2)
XCTAssertFalse(sut.words.isEmpty)
XCTAssertEqual(sut.words.count, 3)
}
func testFilter_forNewOption_withNoItems() async {
let expectation = XCTestExpectation()
var filter: [FilterOption] = []
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .none)
)
// WHEN
sut.$filter
.dropFirst()
.sink { value in
filter.append(value)
expectation.fulfill()
}
.store(in: &cancellables)
await sut.fetch()
sut.filter(by: .only1Star)
wait(for: [expectation], timeout: 2)
// THEN
XCTAssertEqual(filter, [.only1Star])
XCTAssertTrue(sut.items.isEmpty)
XCTAssertEqual(sut.items.count, 0)
XCTAssertEqual(sut.itemsCount, 0)
XCTAssertTrue(sut.words.isEmpty)
XCTAssertEqual(sut.words.count, 0)
}
func testFilter_forSameOption_withSomeItems() async {
let expectation = XCTestExpectation()
var filter: [FilterOption] = []
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
sut.$filter
.dropFirst()
.sink { value in
filter.append(value)
expectation.fulfill()
}
.store(in: &cancellables)
await sut.fetch()
sut.filter(by: .only1Star)
sut.filter(by: .only1Star)
wait(for: [expectation], timeout: 2)
// THEN
XCTAssertEqual(filter, [.only1Star])
XCTAssertFalse(sut.items.isEmpty)
XCTAssertEqual(sut.items.count, 1)
XCTAssertEqual(sut.itemsCount, 2)
XCTAssertFalse(sut.words.isEmpty)
XCTAssertEqual(sut.words.count, 3)
}
func testFilter_forSameOption_withNoItems() async {
let expectation = XCTestExpectation()
var filter: [FilterOption] = []
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .none)
)
// WHEN
sut.$filter
.dropFirst()
.sink { value in
filter.append(value)
expectation.fulfill()
}
.store(in: &cancellables)
await sut.fetch()
sut.filter(by: .only1Star)
wait(for: [expectation], timeout: 2)
// THEN
XCTAssertEqual(filter, [.only1Star])
XCTAssertTrue(sut.items.isEmpty)
XCTAssertEqual(sut.items.count, 0)
XCTAssertEqual(sut.itemsCount, 0)
XCTAssertTrue(sut.words.isEmpty)
XCTAssertEqual(sut.words.count, 0)
}
func testItemFor_index_withSomeItems() async {
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
await sut.fetch()
let item = sut.item(for: 0)
// THEN
XCTAssertNotNil(item)
}
func testItemFor_indexOutOfBounds_withSomeItems() async {
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
await sut.fetch()
let item = sut.item(for: 10)
// THEN
XCTAssertNil(item)
}
func testItemFor_index_withFilteredItems() async {
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
await sut.fetch()
sut.filter(by: .only1Star)
let item = sut.item(for: 1)
// THEN
XCTAssertNotNil(item)
}
func testItemFor_indexOutOfBounds_withFilteredItems() async {
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
await sut.fetch()
sut.filter(by: .only1Star)
let item = sut.item(for: 2)
// THEN
XCTAssertNil(item)
}
func testItemFor_index_withNoItems() async {
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .none)
)
// WHEN
await sut.fetch()
let item = sut.item(for: 0)
// THEN
XCTAssertNil(item)
}
func testOpenItemAt_index_withSomeItems() async {
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
await sut.fetch()
sut.openItem(at: 0)
// THEN
XCTAssertTrue(coordination.itemOpened)
}
func testOpenItemAt_indexOutOfBounds_withSomeItems() async {
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
await sut.fetch()
sut.openItem(at: 10)
// THEN
XCTAssertFalse(coordination.itemOpened)
}
func testOpenItemAt_index_withFilteredItems() async {
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
sut.filter(by: .only1Star)
await sut.fetch()
sut.openItem(at: 1)
// THEN
XCTAssertTrue(coordination.itemOpened)
}
func testOpenItemAt_indexOutOfBounds_withFilteredItems() async {
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .sample)
)
// WHEN
sut.filter(by: .only1Star)
await sut.fetch()
sut.openItem(at: 2)
// THEN
XCTAssertFalse(coordination.itemOpened)
}
func testOpenItemAt_index_withNoItems() async {
// GIVEN
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: .none)
)
// WHEN
await sut.fetch()
sut.openItem(at: 0)
// THEN
XCTAssertFalse(coordination.itemOpened)
}
}

View File

@ -18,6 +18,10 @@
02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E782BAB6B0200710E14 /* FilterOption.swift */; }; 02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E782BAB6B0200710E14 /* FilterOption.swift */; };
02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */; }; 02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */; };
02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */; }; 02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */; };
02B36F7D2BAD9D1A00F1A89D /* ReviewsFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; platformFilter = ios; };
02B36F852BAD9DDB00F1A89D /* FeedListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */; };
02B36F892BADB26C00F1A89D /* Array+ReviewDTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */; };
02B36F8F2BADCABC00F1A89D /* FeedListCoordinationSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F8D2BADCA8000F1A89D /* FeedListCoordinationSpy.swift */; };
02C1B1972BAC9BFE001781DE /* FeedListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */; }; 02C1B1972BAC9BFE001781DE /* FeedListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */; };
02C1B1A92BACA722001781DE /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1A82BACA722001781DE /* AppCoordinator.swift */; }; 02C1B1A92BACA722001781DE /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1A82BACA722001781DE /* AppCoordinator.swift */; };
02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */; }; 02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */; };
@ -40,6 +44,13 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
02B36F7E2BAD9D1A00F1A89D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 345AD11024C6EDD9004E2EE1 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 02DC7F8E2BA51793000EEEBE;
remoteInfo = Feed;
};
02DC7FA02BA51793000EEEBE /* PBXContainerItemProxy */ = { 02DC7FA02BA51793000EEEBE /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 345AD11024C6EDD9004E2EE1 /* Project object */; containerPortal = 345AD11024C6EDD9004E2EE1 /* Project object */;
@ -75,6 +86,10 @@
02909E782BAB6B0200710E14 /* FilterOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterOption.swift; sourceTree = "<group>"; }; 02909E782BAB6B0200710E14 /* FilterOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterOption.swift; sourceTree = "<group>"; };
02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.swift"; sourceTree = "<group>"; }; 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.swift"; sourceTree = "<group>"; };
02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+DTOs.swift"; sourceTree = "<group>"; }; 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+DTOs.swift"; sourceTree = "<group>"; };
02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewModelTests.swift; sourceTree = "<group>"; };
02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+ReviewDTOs.swift"; sourceTree = "<group>"; };
02B36F8D2BADCA8000F1A89D /* FeedListCoordinationSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinationSpy.swift; sourceTree = "<group>"; };
02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinator.swift; sourceTree = "<group>"; }; 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinator.swift; sourceTree = "<group>"; };
02C1B1A82BACA722001781DE /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; }; 02C1B1A82BACA722001781DE /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; }; 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
@ -97,6 +112,14 @@
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
02B36F762BAD9D1A00F1A89D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
02B36F7D2BAD9D1A00F1A89D /* ReviewsFeed.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
02DC7F8C2BA51793000EEEBE /* Frameworks */ = { 02DC7F8C2BA51793000EEEBE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -217,6 +240,7 @@
02DC7F912BA51793000EEEBE /* ReviewsFeed.h */, 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */,
02DA924B2BAAE3E500C47985 /* Resources */, 02DA924B2BAAE3E500C47985 /* Resources */,
02DC7FB02BA51B4F000EEEBE /* Sources */, 02DC7FB02BA51B4F000EEEBE /* Sources */,
02B36F862BADB1FD00F1A89D /* Previews */,
); );
path = Bundle; path = Bundle;
sourceTree = "<group>"; sourceTree = "<group>";
@ -224,10 +248,68 @@
02A6DA302BA5929F00B943E2 /* Test */ = { 02A6DA302BA5929F00B943E2 /* Test */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
02B36F8A2BADCA5B00F1A89D /* Helpers */,
02B36F742BAD9C4500F1A89D /* Tests */,
); );
path = Test; path = Test;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
02B36F742BAD9C4500F1A89D /* Tests */ = {
isa = PBXGroup;
children = (
02B36F832BAD9DC700F1A89D /* View Models */,
);
path = Tests;
sourceTree = "<group>";
};
02B36F832BAD9DC700F1A89D /* View Models */ = {
isa = PBXGroup;
children = (
02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */,
);
path = "View Models";
sourceTree = "<group>";
};
02B36F862BADB1FD00F1A89D /* Previews */ = {
isa = PBXGroup;
children = (
02B36F872BADB23200F1A89D /* Extensions */,
);
path = Previews;
sourceTree = "<group>";
};
02B36F872BADB23200F1A89D /* Extensions */ = {
isa = PBXGroup;
children = (
02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
02B36F8A2BADCA5B00F1A89D /* Helpers */ = {
isa = PBXGroup;
children = (
02B36F8B2BADCA6200F1A89D /* Spies */,
);
path = Helpers;
sourceTree = "<group>";
};
02B36F8B2BADCA6200F1A89D /* Spies */ = {
isa = PBXGroup;
children = (
02B36F8C2BADCA6A00F1A89D /* Coordination */,
);
path = Spies;
sourceTree = "<group>";
};
02B36F8C2BADCA6A00F1A89D /* Coordination */ = {
isa = PBXGroup;
children = (
02B36F8D2BADCA8000F1A89D /* FeedListCoordinationSpy.swift */,
);
path = Coordination;
sourceTree = "<group>";
};
02C1B1952BAC9BE7001781DE /* Coordinators */ = { 02C1B1952BAC9BE7001781DE /* Coordinators */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -364,6 +446,7 @@
children = ( children = (
345AD11824C6EDD9004E2EE1 /* Reviews.app */, 345AD11824C6EDD9004E2EE1 /* Reviews.app */,
02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */, 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */,
02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -391,6 +474,24 @@
/* End PBXHeadersBuildPhase section */ /* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
02B36F782BAD9D1A00F1A89D /* FeedTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 02B36F802BAD9D1A00F1A89D /* Build configuration list for PBXNativeTarget "FeedTests" */;
buildPhases = (
02B36F752BAD9D1A00F1A89D /* Sources */,
02B36F762BAD9D1A00F1A89D /* Frameworks */,
02B36F772BAD9D1A00F1A89D /* Resources */,
);
buildRules = (
);
dependencies = (
02B36F7F2BAD9D1A00F1A89D /* PBXTargetDependency */,
);
name = FeedTests;
productName = ReviewsFeedTests;
productReference = 02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
02DC7F8E2BA51793000EEEBE /* Feed */ = { 02DC7F8E2BA51793000EEEBE /* Feed */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 02DC7FA92BA51793000EEEBE /* Build configuration list for PBXNativeTarget "Feed" */; buildConfigurationList = 02DC7FA92BA51793000EEEBE /* Build configuration list for PBXNativeTarget "Feed" */;
@ -445,6 +546,9 @@
LastUpgradeCheck = 1530; LastUpgradeCheck = 1530;
ORGANIZATIONNAME = "Röck+Cöde"; ORGANIZATIONNAME = "Röck+Cöde";
TargetAttributes = { TargetAttributes = {
02B36F782BAD9D1A00F1A89D = {
CreatedOnToolsVersion = 15.3;
};
02DC7F8E2BA51793000EEEBE = { 02DC7F8E2BA51793000EEEBE = {
CreatedOnToolsVersion = 15.3; CreatedOnToolsVersion = 15.3;
}; };
@ -466,13 +570,21 @@
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
targets = ( targets = (
02DC7F8E2BA51793000EEEBE /* Feed */,
345AD11724C6EDD9004E2EE1 /* App */, 345AD11724C6EDD9004E2EE1 /* App */,
02DC7F8E2BA51793000EEEBE /* Feed */,
02B36F782BAD9D1A00F1A89D /* FeedTests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */ /* Begin PBXResourcesBuildPhase section */
02B36F772BAD9D1A00F1A89D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
02DC7F8D2BA51793000EEEBE /* Resources */ = { 02DC7F8D2BA51793000EEEBE /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -493,11 +605,21 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
02B36F752BAD9D1A00F1A89D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
02B36F852BAD9DDB00F1A89D /* FeedListViewModelTests.swift in Sources */,
02B36F8F2BADCABC00F1A89D /* FeedListCoordinationSpy.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
02DC7F8B2BA51793000EEEBE /* Sources */ = { 02DC7F8B2BA51793000EEEBE /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
02EACF302BABA50D00FF8ECD /* TopWordsCell.swift in Sources */, 02EACF302BABA50D00FF8ECD /* TopWordsCell.swift in Sources */,
02B36F892BADB26C00F1A89D /* Array+ReviewDTOs.swift in Sources */,
02620B8C2BA89C9A00DE7137 /* FeedListViewModel.swift in Sources */, 02620B8C2BA89C9A00DE7137 /* FeedListViewModel.swift in Sources */,
02EACF342BABB28900FF8ECD /* TopWord.swift in Sources */, 02EACF342BABB28900FF8ECD /* TopWord.swift in Sources */,
023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */, 023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */,
@ -532,6 +654,12 @@
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
02B36F7F2BAD9D1A00F1A89D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilter = ios;
target = 02DC7F8E2BA51793000EEEBE /* Feed */;
targetProxy = 02B36F7E2BAD9D1A00F1A89D /* PBXContainerItemProxy */;
};
02DC7FA12BA51793000EEEBE /* PBXTargetDependency */ = { 02DC7FA12BA51793000EEEBE /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 02DC7F8E2BA51793000EEEBE /* Feed */; target = 02DC7F8E2BA51793000EEEBE /* Feed */;
@ -551,6 +679,59 @@
/* End PBXVariantGroup section */ /* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
02B36F812BAD9D1A00F1A89D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 7FMNM89WKG;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.rock-n-code.assignment.ing.framework.feed.test.unit";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
02B36F822BAD9D1A00F1A89D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 7FMNM89WKG;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.rock-n-code.assignment.ing.framework.feed.test.unit";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
02DC7FA52BA51793000EEEBE /* Debug */ = { 02DC7FA52BA51793000EEEBE /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@ -562,6 +743,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = Frameworks/Feed/Bundle/Previews;
DEVELOPMENT_TEAM = 7FMNM89WKG; DEVELOPMENT_TEAM = 7FMNM89WKG;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
@ -611,6 +793,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = Frameworks/Feed/Bundle/Previews;
DEVELOPMENT_TEAM = 7FMNM89WKG; DEVELOPMENT_TEAM = 7FMNM89WKG;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
@ -825,6 +1008,15 @@
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
02B36F802BAD9D1A00F1A89D /* Build configuration list for PBXNativeTarget "FeedTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
02B36F812BAD9D1A00F1A89D /* Debug */,
02B36F822BAD9D1A00F1A89D /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
02DC7FA92BA51793000EEEBE /* Build configuration list for PBXNativeTarget "Feed" */ = { 02DC7FA92BA51793000EEEBE /* Build configuration list for PBXNativeTarget "Feed" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (