This commit is contained in:
Igor Popov 2017-09-20 22:49:50 +03:00
Родитель cd4b4ff451
Коммит 0d394c2a27
9 изменённых файлов: 247 добавлений и 46 удалений

Просмотреть файл

@ -1,2 +1,4 @@
#! /bin/bash #! /bin/bash
cp -R EmbeddedSocial\ Swift\ File.xctemplate ~/Library/Developer/Xcode/Templates/Custom/ destination=$HOME"/Library/Developer/Xcode/Templates/Custom/"
mkdir -p $destination
cp -R EmbeddedSocial\ Swift\ File.xctemplate $destination

Просмотреть файл

@ -786,6 +786,7 @@
EB45E0D87E3DCF49B5A22162 /* CommentRepliesInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC43B539B5A91FF6D4EAB9B /* CommentRepliesInitializer.swift */; }; EB45E0D87E3DCF49B5A22162 /* CommentRepliesInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC43B539B5A91FF6D4EAB9B /* CommentRepliesInitializer.swift */; };
EC75A126A73CC5F294926613 /* CommentRepliesConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C6D69159F11B9D4D3F7000 /* CommentRepliesConfigurator.swift */; }; EC75A126A73CC5F294926613 /* CommentRepliesConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C6D69159F11B9D4D3F7000 /* CommentRepliesConfigurator.swift */; };
ED9BC53643CA7A3E7A3106DC /* CommentCellConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F824A8069868818F477BD /* CommentCellConfigurator.swift */; }; ED9BC53643CA7A3E7A3106DC /* CommentCellConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F824A8069868818F477BD /* CommentCellConfigurator.swift */; };
F8ED92AE1F72DF3E0026AC3E /* ActivityPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8ED92AD1F72DF3E0026AC3E /* ActivityPresenterTests.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -1631,6 +1632,7 @@
E63643937A3B405C1B4E0B49 /* CommentCellInteractorInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentCellInteractorInput.swift; sourceTree = "<group>"; }; E63643937A3B405C1B4E0B49 /* CommentCellInteractorInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentCellInteractorInput.swift; sourceTree = "<group>"; };
EC1ECDB428A92745248FE72C /* CommentCellRouterInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentCellRouterInput.swift; sourceTree = "<group>"; }; EC1ECDB428A92745248FE72C /* CommentCellRouterInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentCellRouterInput.swift; sourceTree = "<group>"; };
F35AC4A39014F4C18D39AA62 /* ActivityRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ActivityRouter.swift; sourceTree = "<group>"; }; F35AC4A39014F4C18D39AA62 /* ActivityRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ActivityRouter.swift; sourceTree = "<group>"; };
F8ED92AD1F72DF3E0026AC3E /* ActivityPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityPresenterTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -4338,6 +4340,7 @@
children = ( children = (
9CEE0EDD1F6FE3F7008B1104 /* ActivityInteractorTests.swift */, 9CEE0EDD1F6FE3F7008B1104 /* ActivityInteractorTests.swift */,
9CCE405C1F72827A003A51D9 /* ActivityTests.swift */, 9CCE405C1F72827A003A51D9 /* ActivityTests.swift */,
F8ED92AD1F72DF3E0026AC3E /* ActivityPresenterTests.swift */,
); );
path = Activity; path = Activity;
sourceTree = "<group>"; sourceTree = "<group>";
@ -5765,6 +5768,7 @@
881F7E621F263888003EB37A /* MockKeyValueStorage.swift in Sources */, 881F7E621F263888003EB37A /* MockKeyValueStorage.swift in Sources */,
88B94ED11F62C3FA002392F9 /* MockSearchTopicsInteractor.swift in Sources */, 88B94ED11F62C3FA002392F9 /* MockSearchTopicsInteractor.swift in Sources */,
9C985E531F505BDF00514F85 /* FeedCacheTests.swift in Sources */, 9C985E531F505BDF00514F85 /* FeedCacheTests.swift in Sources */,
F8ED92AE1F72DF3E0026AC3E /* ActivityPresenterTests.swift in Sources */,
884C9C341F4C3E940004907F /* UserListPresenterTests.swift in Sources */, 884C9C341F4C3E940004907F /* UserListPresenterTests.swift in Sources */,
932898D41F65929F001F3BC2 /* ReportReplyAPITests.swift in Sources */, 932898D41F65929F001F3BC2 /* ReportReplyAPITests.swift in Sources */,
9C30243C1F55AB2100675FE9 /* FeedModuleRouterMock.swift in Sources */, 9C30243C1F55AB2100675FE9 /* FeedModuleRouterMock.swift in Sources */,

Просмотреть файл

@ -8,19 +8,23 @@ import Foundation
protocol SectionModelType { protocol SectionModelType {
associatedtype Item associatedtype Item
var items: [Item] { get } var items: [Item] { get set }
} }
struct SectionModel<Section, ItemType>: SectionModelType { struct SectionModel<Section, ItemType>: SectionModelType {
typealias Item = ItemType typealias Item = ItemType
let model: Section var model: Section
let items: [Item] var items: [Item]
init(model: Section, items: [Item]) { init(model: Section, items: [Item]) {
self.model = model self.model = model
self.items = items self.items = items
} }
mutating func erase() {
items = []
}
} }
extension SectionModel: CustomStringConvertible { extension SectionModel: CustomStringConvertible {

Просмотреть файл

@ -91,6 +91,12 @@ struct PendingRequestItem {
var userHandle: String var userHandle: String
} }
extension PendingRequestItem {
static func mock(seed: Int) -> PendingRequestItem {
return PendingRequestItem(userName: "username \(seed)", userHandle: "handle \(seed)")
}
}
// Helpers // Helpers
enum Change<T> { enum Change<T> {
@ -166,7 +172,7 @@ class ActivityViewModelBuilder {
class SectionsConfigurator { class SectionsConfigurator {
func build(section: ActivityPresenter.State) -> [ActivityPresenter.Section] { func build(section: ActivityPresenter.State) -> [Section] {
switch section { switch section {
case .my: case .my:
@ -174,21 +180,22 @@ class SectionsConfigurator {
case .others: case .others:
return others return others
} }
} }
private var my: [ActivityPresenter.Section] { private var my: [Section] {
let sectionHeader = SectionHeader(name: "Section 1", identifier: "") let sectionHeader = SectionHeader(name: "Section 1", identifier: "")
let model = PendingRequestItem(userName: "User", userHandle: "User handle") let model = PendingRequestItem(userName: "User", userHandle: "User handle")
let item = ActivityItem.pendingRequest(model) let item = ActivityItem.pendingRequest(model)
let section = ActivityPresenter.Section(model: sectionHeader, items: [item]) let section = Section(model: sectionHeader, items: [item])
return [section] return [section]
} }
private var others: [ActivityPresenter.Section] { private var others: [Section] {
let sectionHeader = SectionHeader(name: "Section 2", identifier: "") let sectionHeader = SectionHeader(name: "Section 2", identifier: "")
let model = ActionItem.mock(seed: 0) let model = ActionItem.mock(seed: 0)
let item = ActivityItem.follower(model) let item = ActivityItem.follower(model)
let section = ActivityPresenter.Section(model: sectionHeader, items: [item]) let section = Section(model: sectionHeader, items: [item])
return [section] return [section]
} }

Просмотреть файл

@ -11,6 +11,10 @@ protocol ActivityInteractorOutput: class {
protocol ActivityInteractorInput { protocol ActivityInteractorInput {
func loadAll()
func loadNextPageFollowingActivities(completion: ((Result<[ActionItem]>) -> Void)?)
func loadNextPagePendigRequestItems(completion: ((Result<[PendingRequestItem]>) -> Void)?)
} }
protocol ActivityService: class { protocol ActivityService: class {
@ -190,37 +194,39 @@ extension ActivityInteractor: ActivityInteractorInput {
let pageID = UUID().uuidString let pageID = UUID().uuidString
loadingPages.insert(pageID) loadingPages.insert(pageID)
service.loadFollowingActivities(cursor: followersList.cursor, service.loadFollowingActivities(
limit: followersList.limit) { [weak self] (result: Result<FeedResponseActivityView>) in cursor: followersList.cursor,
limit: followersList.limit) { [weak self] (result: Result<FeedResponseActivityView>) in
defer {
loadingPages.remove(pageID) defer {
} loadingPages.remove(pageID)
}
// exit on released or canceled
guard let strongSelf = self, strongSelf.loadingPages.contains(pageID) else { // exit on released or canceled
return guard let strongSelf = self, strongSelf.loadingPages.contains(pageID) else {
} return
}
// must have data
guard let response = result.value else { // must have data
completion?(.failure(ActivityError.noData)) guard let response = result.value else {
return completion?(.failure(ActivityError.noData))
} return
}
// map data into page
guard let page = strongSelf.process(response: response, pageID: pageID) else { // map data into page
completion?(.failure(ActivityError.notParsable)) guard let page = strongSelf.process(response: response, pageID: pageID) else {
return completion?(.failure(ActivityError.notParsable))
} return
}
strongSelf.followersList.add(page: page)
strongSelf.followersList.add(page: page)
completion?(.success(page.items))
completion?(.success(page.items))
} }
} }
// TODO: remake using generics
func loadNextPagePendigRequestItems(completion: ((Result<[PendingRequestItem]>) -> Void)? = nil) { func loadNextPagePendigRequestItems(completion: ((Result<[PendingRequestItem]>) -> Void)? = nil) {
let pageID = UUID().uuidString let pageID = UUID().uuidString

Просмотреть файл

@ -7,14 +7,14 @@ protocol ActivityModuleInput: class {
} }
typealias Section = SectionModel<SectionHeader, ActivityItem>
class ActivityPresenter { class ActivityPresenter {
typealias Section = SectionModel<SectionHeader, ActivityItem>
weak var view: ActivityViewInput! weak var view: ActivityViewInput!
var interactor: ActivityInteractorInput! var interactor: ActivityInteractorInput!
var router: ActivityRouterInput! var router: ActivityRouterInput!
enum State: Int { enum State: Int {
case my case my
case others case others
@ -44,9 +44,9 @@ class ActivityPresenter {
sections[.my] = sectionsConfigurator.build(section: .my) sections[.my] = sectionsConfigurator.build(section: .my)
sections[.others] = sectionsConfigurator.build(section: .others) sections[.others] = sectionsConfigurator.build(section: .others)
} }
fileprivate func loadNextPage() { fileprivate func loadNextPage() {
} }
} }
@ -59,13 +59,98 @@ extension ActivityPresenter: ActivityInteractorOutput {
} }
protocol DataSourceProtocol {
func loadMore()
var section: Section { get }
}
class DataSource: DataSourceProtocol {
var interactor: ActivityInteractorInput
var section: Section
var errorHandler: ((Error) -> Void)?
func loadMore() { }
init(interactor: ActivityInteractorInput, section: Section, errorHandler: ((Error) -> Void)? = nil) {
self.interactor = interactor
self.section = section
self.errorHandler = errorHandler
}
}
class MyPendingRequests: DataSource {
override func loadMore() {
// load pendings
interactor.loadNextPagePendigRequestItems { [weak self] (result) in
switch result {
case let .failure(error):
self?.errorHandler?(error)
case let .success(models):
let items = models.map { ActivityItem.pendingRequest($0) }
self?.section.items.append(contentsOf: items)
}
}
}
}
class MyFollowersActivity: DataSource {
override func loadMore() {
// load activity
interactor.loadNextPageFollowingActivities { [weak self] (result) in
switch result {
case let .failure(error):
self?.errorHandler?(error)
case let .success(models):
let items = models.map { ActivityItem.follower($0) }
self?.section.items.append(contentsOf: items)
}
}
}
}
class MyFollowingsActivity: DataSource {
override func loadMore() {
}
}
extension ActivityPresenter: ActivityViewOutput { extension ActivityPresenter: ActivityViewOutput {
func load() { func load() {
interactor.loadAll()
} }
func loadMore(for section: Int) { func loadMore() {
interactor.loadNextPageFollowingActivities { (result) in
switch result {
case let .success(items):
// sections[state]
break
case let .failure(error):
break
}
}
interactor.loadNextPageFollowingActivities { (result) in
}
} }
@ -86,7 +171,7 @@ extension ActivityPresenter: ActivityViewOutput {
} }
func numberOfSections() -> Int { func numberOfSections() -> Int {
return sections.count return sections[state]!.count
} }
func numberOfItems(in section: Int) -> Int { func numberOfItems(in section: Int) -> Int {

Просмотреть файл

@ -7,7 +7,8 @@ import UIKit
protocol ActivityViewInput: class { protocol ActivityViewInput: class {
func setupInitialState() func setupInitialState()
func registerCell(cell: AnyObject.Type, id: String) func registerCell(cell: UITableViewCell.Type, id: String)
func showError(_ error: Error)
} }
protocol ActivityViewOutput: class { protocol ActivityViewOutput: class {
@ -44,11 +45,15 @@ class ActivityViewController: UIViewController {
extension ActivityViewController: ActivityViewInput { extension ActivityViewController: ActivityViewInput {
func showError(_ error: Error) {
Logger.log(error, event: .veryImportant)
}
func setupInitialState() { func setupInitialState() {
} }
func registerCell(cell: AnyObject.Type, id: String) { func registerCell(cell: UITableViewCell.Type, id: String) {
} }

Просмотреть файл

@ -0,0 +1,69 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
//
import XCTest
@testable import EmbeddedSocial
class ActivityInteractorMock: ActivityInteractorInput {
var followingActivityItemsResult: Result<[ActionItem]>!
var pendingRequestsItemsResult: Result<[PendingRequestItem]>!
func loadAll() {
}
func loadNextPageFollowingActivities(completion: ((Result<[ActionItem]>) -> Void)? = nil) {
completion?(followingActivityItemsResult)
}
func loadNextPagePendigRequestItems(completion: ((Result<[PendingRequestItem]>) -> Void)? = nil) {
completion?(pendingRequestsItemsResult)
}
}
class ActivityPresenterTests: XCTestCase {
var sut: ActivityPresenter!
var interactor : ActivityInteractorMock!
override func setUp() {
super.setUp()
sut = ActivityPresenter()
sut.interactor = ActivityInteractorMock()
}
override func tearDown() {
super.tearDown()
}
func test() {
// given
let mockItems = Array(1..<5).map { PendingRequestItem.mock(seed: $0) }
let result: Result<[PendingRequestItem]> = .success(mockItems)
let header = SectionHeader(name: "", identifier: "")
let section = Section(model: header, items: [])
let pendingRequestsDataSource = MyPendingRequests(interactor: interactor,
section: section)
interactor.pendingRequestsItemsResult = result
pendingRequestsDataSource.loadMore()
pendingRequestsDataSource.section.items.count
}
}

Просмотреть файл

@ -87,6 +87,25 @@ class ActivityEntitiesTests: XCTestCase {
waitForExpectations(timeout: 1, handler: nil) waitForExpectations(timeout: 1, handler: nil)
} }
func testThatPaggingWorksCorrectly() {
// given
let service = ActivityServiceMock()
let followingActivitiesResponse = buildActivitiResponseMock()
service.followingActivitiesResponse = .success(followingActivitiesResponse)
let interactor = ActivityInteractor()
interactor.service = service
let pendingRequests = expectation(description: #file)
// when
// interactor.
// then
}
func buildActivitiResponseMock() -> FeedResponseActivityView { func buildActivitiResponseMock() -> FeedResponseActivityView {
return FeedResponseActivityView().mockResponse() return FeedResponseActivityView().mockResponse()
} }