Teller
正在构建一个从网络获取数据的iOS应用程序?使用Teller,您可以在几分钟内为您的iOS应用程序添加缓存!更快的应用程序,为更好的用户体验。
阅读Teller的官方公告,了解更多关于它的功能以及为什么应该使用它的信息。
Android开发者? 请查看Teller的Android版本。
什么是Teller?
缓存您应用从网络调用获取的数据可以让您的应用使用起来更加愉悦。
- 减少加载屏幕。当您的应用程序用户打开应用时,他们可以浏览缓存,而无需等待加载屏幕。
- 帮助您的用户更快地完成任务。当您的用户打开应用时,他们希望在几秒钟内完成任务。通过使用缓存,您可以更快地向用户展示数据,以便他们完成任务。
- 当您使用缓存时,您可以利用在后台更新应用的机制,这样当您的用户未来再次打开应用时,他们将看到最新的数据。
但是,将缓存添加到您的应用需要工作量。您需要...
- 从网络获取、保存和查询缓存。
- 解析缓存以确定其状态。
- 确保您不需要不必要地执行获取来更新设备获取(以节省用户电量),但同时您也不希望获取过于频繁,否则缓存会过时。
- 处理无休止的网络数据分页。
Teller负责上述所有任务(除了第1项,那部分是您的责任)。您需要做的只是告诉Teller如何获取、保存和查询设备缓存,其余的由Teller负责。
这可以让您在几分钟内将缓存添加到您的应用,无需编写管理代码。
您是否正在开发一款离线优先的移动应用?
Teller专为构建离线优先移动应用的开发者设计。如果您正在寻找构建离线优先移动应用,也请务必查看Wendy(还有Android版本)。Wendy旨在同步您的设备缓存数据与远程存储。想象一下:Teller非常擅长网络API的GET
调用,Wendy非常擅长网络API的PUT, POST, DELETE
调用。Teller拉取数据,Wendy推送数据。这两个库配合得真的很好!
为什么要使用Teller?
不仅Teller可以帮助您快速轻松地为您的应用添加缓存,Teller还允许您使您的应用对用户更加透明。您将能够轻松地告诉您的用户...
- 缓存有多久的历史。
- 他们现在查看的本地缓存是否正在更新(通过网络调用)或不是。
- 是否有获取,是否有错误或不是。
- 缓存是否曾经成功获取。
您在应用中添加缓存时,透明度对于用户理解缓存状态非常重要。Teller会为您处理这一切。
Teller还有一些其他的好处
- 小。截至目前,唯一的依赖项是RxSwift(请关注此问题,我会努力去除这个依赖项并使其可选)。Teller旨在完成一项工作并且做得很好,那就是:缓存从网络检索的数据。
- 用Swift构建,由Swift编写。Teller是用Swift编写的,这意味着您可以期待一个易用的API。
- 无偏见。Teller不关心您的数据存储在哪里或如何查询。您只需告诉Teller何时完成获取、保存和查询,Teller就会负责将其传递给听众。
- 经过良好测试。目前已在生产应用中运行,并在整个代码库中进行单元/集成测试。
完整文档(即将推出。在此期间,此README文件包含所有必要的文档)- 为使用分页的网络调用添加缓存。
安装
您可以通过 CocoaPods 获取 Teller。要安装它,只需在您的 Podfile 中添加以下行:
pod 'Teller', '~> version-here'
将 version-here
替换为: ,因为这是此时的最新版本。
注意: Teller正在开发中。虽然它在我的应用中实际用于生产,但代码库可能随时发生变化。
使用Teller有几年时间了,随着时间的推移,我已经能够使这个库逐渐成熟。API仍然被认为是阿尔法版本,因为将来可能会有重大变化。
入门指南
Teller的设计目标是:帮助您快速轻松地将缓存支持添加到您的应用中。您帮助Teller了解缓存在哪里保存,如何获取它,而Teller则负责其他一切。让我们开始吧。
注意:如果您希望使用Teller进行分页,请首先阅读这份入门指南,然后阅读分页部分了解如何进行。
- 首先,创建一个
RepositoryDataSource
的实现。
以下是一个示例。
import Foundation
import Teller
import RxSwift
import Moya
class ReposRequirements: RepositoryRequirements {
/**
The tag is used to determine how old your cache is. Teller uses this to determine if a fresh cache needs to be fetched or not. If the tag matches previously cached data of the same tag, the data that data was fetched will be queried and determined if it's considered too old and will fetch fresh data or not from the result of the compare.
The best practice is to describe what the cache represents. "Repos for <username>" is a great example.
*/
var tag: ReposRepositoryRequirements.Tag {
return "Repos for \(username)"
}
let username: String
init(username: String) {
self.username = username
}
}
// Struct used to represent the JSON data pulled from the GitHub API.
struct Repo: Codable {
var id: Int!
var name: String!
}
class ReposRepositoryDataSource: RepositoryDataSource {
typealias Cache = [Repo]
typealias Requirements = ReposRepositoryRequirements
typealias FetchResult = [Repo]
typealias FetchError = Error
// How old a cache can be before it's considered old and an automatic refresh should be performed.
// Teller tries to reduce the number of network calls performed to save on bandwidth of your user's device.
var maxAgeOfCache: Period = Period(unit: 5, component: .hour)
func fetchFreshCache(requirements: Requirements) -> Single<FetchResponse<Cache, FetchError>> {
// Return network call that returns a RxSwift Single.
// The project Moya (https://github.com/moya/moya) is my favorite library to do this.
return MoyaProvider<GitHubService>().rx.request(.listRepos(user: requirements.username))
.map({ (response) -> FetchResponse<[Repo]> in
let repos = try! JSONDecoder().decode([Repo].self, from: response.data)
// If there was a failure, use FetchResponse.failure(Error) and the error will be sent to your user in the UI
return FetchResponse.success(data: repos)
})
}
// Note: Teller runs this function from a background thread.
func saveCache(_ fetchedData: Cache, requirements: Requirements) throws {
// Save data to CoreData, Realm, UserDefaults, File, whatever you wish here.
// If there is an error, you may throw it, and have it get passed to the observer of the Repository.
}
// Note: Teller runs this function from the UI thread
func observeCache(requirements: Requirements) -> Observable<Cache> {
// Return Observable that is observing the cached data.
//
// When any of the repos in the database have been changed, we want to trigger an Observable update.
// Teller may call `observeCachedData` regularly to keep data fresh.
return Observable.just([])
}
// Note: Teller runs this function from the same thread as `observeCachedData()`
func isCacheEmpty(_ cache: Cache, requirements: Requirements) -> Bool {
return cache.isEmpty
}
}
- 最后一步。观察您的缓存。用
TellerRepository
实例来完成这个操作。
let disposeBag = DisposeBag()
let repository = TellerRepository(dataSource: ReposRepositoryDataSource())
repository.requirements = ReposRepositoryDataSource.Requirements(username: "username to get repos for")
repository
.observe()
.observeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.subscribeOn(MainScheduler.instance)
.subscribe(onNext: { (cacheState: CacheState<[Repo]>) in
// Teller provides a handy way to parse the `CacheState` to understand the state your cache is in.
switch cacheState.state {
// No cache exists. A successful network request has not happened yet.
case .noCache:
// Repos have never been fetched before for the GitHub user.
break
case .cache(let cache, let cacheAge):
// Repos have been fetched before for the GitHub user.
let isCacheEmpty = cache == nil // If `cache` is nil, the cache is empty.
// Use `cacheAge` in your UI to tell the user how long ago the last successful network request was.
}
// You can inspect a lot more about the state of your cache.
cacheState.isRefreshing // If a network request is happening right now to refresh the cache
cacheState.justFinishedFirstFetch // The first successful network call just finished
cacheState.refreshError // Get error from network call if there was one during refresh
// ... and more. Use these properties in the UI of your app to be transparent about your cache!
})
.disposed(by: disposeBag)
为了让Teller施展魔力,您需要(1)初始化 requirements
属性,并且(2)observe()
TellerRepository
实例。这给Teller提供了开始所需的信息。如果您忘记设置requirements
,那么在您observe()
时将不会发生任何事情。
完成了!您正在使用Teller!继续阅读本指南以了解高级用法。
分页
假设您已阅读了入门部分。本部分将在该基础上进行扩展。
Teller为在您的应用的缓存中添加分页做了所有艰苦的工作。您需要做的只是根据到目前为止您学习到的如何使用Teller,添加一些更多功能。
- 首先,创建一个
PagingRepositoryDataSource
的实现。这个示例将基于入门部分中的 DataSource。
// `PagingRepositoryRequirements` is a special object used to understand how to fetch pages of data from a network.
// Some APIs you work with might
// ...use a page number, like GitHub: https://developer.github.com/v3/guides/traversing-with-pagination/
// ...use an ID of your last retrieved item, like SoundCloud https://developers.soundcloud.com/docs/api/guide#pagination
// ...use an ID of the first and last retrieved item, like Twitter https://developer.twitter.com/en/docs/ads/general/guides/pagination
//
// Whatever your API uses, you put those properties in this object to keep track of what page you are viewing now.
struct ReposPagingRequirements: PagingRepositoryRequirements {
let pageNumber: Int
func nextPage() -> ReposPagingRequirements {
return ReposPagingRequirements(pageNumber: pageNumber + 1)
}
}
class ReposRepositoryDataSource: PagingRepositoryDataSource {
// The data type your cache is. What `observe()` will use.
typealias PagingCache = [Repo]
// `RepositoryRequirements` subclass you're using
typealias Requirements = ReposRepositoryRequirements
// `PagingRepositoryRequirements` subclass you're using
typealias PagingRequirements = ReposPagingRequirements
// If you're using an API like Twitter or SoundCloud where future network calls depend
// data discovered from the previous network call, this field takes care of that.
// Use an Int, String, Tuple, Struct, etc for this.
// The GitHub API will simply go to the next page number so this field is not used.
typealias NextPageRequirements = Void
// The data type returned from network calls.
typealias PagingFetchResult = [Repo]
// Use a custom Error for network calls in your fetch calls.
typealias FetchError = Error
static let reposPageSize = 50
// You can use whatever method you wish for performing a HTTP network call. Moya is used in this example.
let moyaProvider = MoyaProvider<GitHubService>(plugins: [HttpLoggerMoyaPlugin()])
let keyValueStorage = UserDefaultsKeyValueStorage(userDefaults: UserDefaults.standard)
var maxAgeOfCache: Period = Period(unit: 5, component: .hour)
var currentRepos: [Repo] {
guard let currentReposData = keyValueStorage.string(forKey: .repos)?.data else {
return []
}
return try! JSONDecoder().decode([Repo].self, from: currentReposData)
}
// When you call `goToNextPage()` on your `TellerPagingRepository`, this function is called to get a new `PagingRequirements` for the next network call.
func getNextPagePagingRequirements(currentPagingRequirements: PagingRequirements, nextPageRequirements: NextPageRequirements?) -> PagingRequirements {
return currentPagingRequirements.nextPage()
}
// Teller will call this automatically when it needs. You need to delete all of your cache for the given `Requirements`.
// Note: Teller runs this function from a background thread.
func deleteCache(_ requirements: Requirements) {
keyValueStorage.delete(key: .repos)
}
// Teller will call this automatically when it needs. You need to delete all of your cache for the given `Requirements` *except* for the first page of cache.
// Note: Teller runs this function from a background thread.
func persistOnlyFirstPage(requirements: ReposRepositoryRequirements) {
let currentRepos = self.currentRepos
guard currentRepos.count > ReposRepositoryDataSource.reposPageSize else {
return
}
let firstPageRepos = Array(currentRepos[0...ReposRepositoryDataSource.reposPageSize])
keyValueStorage.setString((try! JSONEncoder().encode(firstPageRepos)).string!, forKey: .repos)
}
// The network call function has changed in the return type that you return.
func fetchFreshCache(requirements: ReposRepositoryRequirements, pagingRequirements: PagingRequirements) -> Single<FetchResponse<FetchResult, Error>> {
// Return network call that returns a RxSwift Single.
// The project Moya (https://github.com/moya/moya) is my favorite library to do this.
return moyaProvider.rx.request(.listRepos(user: requirements.username, pageNumber: pagingRequirements.pageNumber))
.map { (response) -> FetchResponse<FetchResult, FetchError> in
let repos = try! JSONDecoder().decode([Repo].self, from: response.data)
let responseHeaders = response.response!.allHeaderFields
let paginationNext = responseHeaders["link"] as? String ?? responseHeaders["Link"] as! String
let areMorePagesAvailable = paginationNext.contains("rel=\"next\"")
// When using pagination, Teller requires your fetch function to return more information regarding the network calls.
// You need to determine if there are more pages to fetch, or not.
// Also, populate `nextPageRequirements` with whatever you want that will get passed to `getNextPagePagingRequirements` when you call `goToNextPage()`.
//
// If there was a failure, use FetchResponse.failure(Error) and the error will be sent to your user in the UI
return FetchResponse.success(PagedFetchResponse(areMorePages: areMorePagesAvailable, nextPageRequirements: nil, fetchResponse: repos))
}
}
// Save the cache. Friendly reminder to *append* the new cache to storage. You don't want to replace the cache you already have as pagination builds on top of each other.
// Note: Teller runs this function from a background thread.
func saveCache(_ cache: [Repo], requirements: ReposRepositoryRequirements, pagingRequirements: PagingRequirements) throws {
// Save data to CoreData, Realm, UserDefaults, File, whatever you wish here.
// If there is an error, you may throw it, and have it get passed to the observer of the Repository.
var combinedRepos = currentRepos
combinedRepos.append(contentsOf: cache)
keyValueStorage.setString((try! JSONEncoder().encode(combinedRepos)).string!, forKey: .repos)
}
// This function has not changed from the getting started guide.
// Note: Teller runs this function from the UI thread
func observeCache(requirements: ReposRepositoryRequirements, pagingRequirements: ReposPagingRequirements) -> Observable<PagingCache> {
// Return Observable that is observing the cached data.
//
// When any of the repos in the database have been changed, we want to trigger an Observable update.
// Teller may call `observeCachedData` regularly to keep data fresh.
return keyValueStorage.observeString(forKey: .repos)
.map { (string) -> PagingCache in
try! JSONDecoder().decode([Repo].self, from: string.data!)
}
}
// This function has not changed from the getting started guide.
// Note: Teller runs this function from the same thread as `observeCachedData()`
func isCacheEmpty(_ cache: [Repo], requirements: ReposRepositoryRequirements, pagingRequirements: ReposPagingRequirements) -> Bool {
return cache.isEmpty
}
}
- 最后一步。观察您的缓存。用
TellerPagingRepository
实例来完成这个操作。
let disposeBag = DisposeBag()
let repository = TellerPagingRepository(dataSource: ReposRepositoryDataSource(), firstPageRequirements: ReposRepositoryDataSource.PagingRequirements(pageNumber: 1))
let reposGetDataRequirements = ReposRepositoryDataSource.Requirements(username: "username to get repos for")
repository.requirements = reposGetDataRequirements
repository
.observe()
.observeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.subscribeOn(MainScheduler.instance)
.subscribe(onNext: { (dataState: CacheState<PagedCache<[Repo]>>) in
switch dataState.state {
case .noCache:
// Repos have never been fetched before for the GitHub user.
break
case .cache(let cache, let cacheAge):
// Repos have been fetched before for the GitHub user.
// If `cache` is nil, the cache is empty.
if let pagedCache = cache {
let repositories = pagedCache.cache
let areMorePages = pagedCache.areMorePages
// Show/hide a "Loading more" footer in your UITableView by `areMorePages` value.
} else {
// The cache is empty! There are no repos for that particular username.
// Display a view in your app that tells the user there are no repositories to show.
}
// Access all of the properties you're used to when not using pagination:
cacheState.isRefreshing
}
})
.disposed(by: disposeBag)
// When the `UITableView` is scrolled down to the bottom, do...
repository.goToNextPage()
// ...and the next page of cache will be fetched!
保持应用数据更新
当用户打开您的应用时,他们希望看到最新的数据,而不是过时的数据。为此,最好在您的应用处于后台时执行后台刷新。
报账员提供了一种简单方法在后台刷新 TellerRepository
的缓存。您可以根据需要频繁调用此函数。当缓存过时时,报账员才会进行新的取回操作。
let repository = TellerRepository(dataSource: ReposRepositoryDataSource())
repository.requirements = ReposRepositoryDataSource.Requirements(username: "username to get repos for")
try! repository.refresh(force: false)
.subscribe()
注意:您可以使用 iOS 中的 后台应用刷新 特性来定期在一系列 TellerRepository
上运行 refresh
。
仅在不存在现有缓存时进行刷新
如果您的应用在没有缓存的情况下无法工作,请使用方便的 refreshIfNoCache()
函数在仅当该数据源不存在缓存时执行 refresh
调用。当您的应用在全新安装后首次启动时,这是一种很好的方式,以下载缓存来使您的应用工作。
调用 refreshIfNoCache()
,当响应为 .successful
时,您知道已存在缓存。如果已经存在缓存,则 .successful
将立即返回;如果执行刷新操作后存在缓存,则异步返回。
let repository = TellerRepository(dataSource: ReposRepositoryDataSource())
repository.requirements = ReposRepositoryDataSource.Requirements(username: "username to get repos for")
try! repository.refreshIfNoCache()
.subscribe(onSuccess: { (refreshResult) in
if case .successful = refreshResult {
// Cache does exist.
} else {
// Cache does not exist. View the error in `refreshResult` to see why the refresh attempt failed.
}
})
注意:缓存的存在并不能确定缓存是否为空。如果一个缓存已经成功获取过至少一次,则认为该缓存存在。
手动刷新缓存
您有一个启用下拉刷新的 UITableView
吗?您是否在 UINavigationBar
中有一个按钮,希望当按钮被按下时用户可以刷新数据?
没有问题。告诉您的 Teller TellerRepository
实例强制刷新
let repository = TellerRepository(dataSource: ReposRepositoryDataSource())
repository.requirements = ReposRepositoryDataSource.Requirements(username: "username to get repos for")
repository.refresh(force: true)
.subscribe()
转换缓存类型
如果您发现了一个 CacheState<A>
的实例,并且想将其转换为类型 CacheState<B>
,这就是您要做的
repository.observe()
.map { (dataState) -> CacheState<B> in
dataState.convert { (a) -> B? in
guard let a = a else { return nil }
B(a: a)
}
}
相当简单。当您观察了一个 TellerRepository
时,对 DataState
的实例调用 convert
来将其转换为不同的缓存类型。
启用和禁用自动刷新功能
Teller的一个便利之处是它在适当的时候(例如:1)在RepositoryDataSource
的实例上设置了新的要求时,(2)调用TellerRepository.observe()
或(3)从RepositoryDataSource
触发缓存更新时),为你定期调用TellerRepository.refresh(force: false)
(请注意,自动刷新不会强迫尊重maxAgeOfCache
以保持网络调用到最小)。这样做很方便,因为它有助于保持缓存始终是最新的。
由于这个功能很方便,Teller默认启用了此功能。但是,如果您想禁用此功能,可以在您的RepositoryDataSource
中做到这一点。
class ReposRepositoryDataSource: RepositoryDataSource {
// override default value.
var automaticallyRefresh: Bool {
return false
}
}
建议保留默认功能并启用此功能。然而,有时您可能需要控制网络调用执行的频率。您可能会禁用此功能的场景。
注意:如果您决定禁用此自动刷新功能,那么您就有责任通过在您的应用中定期手动调用TellerRepository.refresh()
来保持您的RepositoryDataSource
的缓存是最新的。
测试
Teller是为了构建单元/集成/UI测试而设计的。以下是您如何在测试中使用Teller的方法。
RepositoryDataSource
或PagingRepositoryDataSource
实现编写单元测试
针对您对RepositoryDataSource
或PagingRepositoryDataSource
的实现不会有问题。《RepositoryDataSource》和《Paging RepositoryDataSource》都是协议。您可以使用依赖注入等工具对您的实现进行单元测试,例如测试《RepositoryDataSource》或《Paging RepositoryDataSource》的所有功能。
TellerRepository
或TellerPagingRepository
的代码的单元测试
编写依赖于对于使用Teller的TellerRepository
或TellerPagingRepository
类的应用代码,使用预构建的TellerRepositoryMock
或TellerPagingRepositoryMock
进行单元测试。使用依赖注入将模拟对象注入到待测类中。
以下是一个针对依赖于Teller的TellerRepository
的类的XCTest示例(使用相同的概念处理TellerPagingRepository
,除了使用TellerPagingRepositoryMock
)。
import RxBlocking
import RxSwift
@testable import YourApp
import XCTest
class RepositoryViewModelTest: XCTestCase {
var viewModel: ReposViewModel!
var repository: RepositoryMock<ReposRepositoryDataSource>!
override func setUp() {
// Create an instance of `RepositoryMock`
repository = TellerRepositoryMock(dataSource: ReposRepositoryDataSource())
// Provide the repository mock to your code under test with dependency injection
viewModel = ReposViewModel(reposRepository: repository)
}
func test_observeRepos_givenReposRepositoryObserve_expectReceiveCacheFromReposRepository() {
// 1. Setup the mock
let given: CacheState<[Repo]> = DataState.testing.cache(requirements: ReposRepositoryDataSource.Requirements(username: "username"), lastTimeFetched: Date()) {
$0.cache([
Repo(id: 1, name: "repo-name")
])
}
repository.observeClosure = {
return Observable.just(given)
}
// 2. Run the code under test
let actual = try! repository.observe().toBlocking().first()
// 3. Assert your code under test is working
XCTAssertEqual(given, actual)
}
func test_setReposToObserve_givenUsername_expectSetRepositoryRequirements() {
let given = "random-username"
// Run your code under test
viewModel.setReposToObserve(username: given)
// Pull out the properties of the repository mock to see if your code under test works as expected
XCTAssertTrue(repository.requirementsCalled)
XCTAssertEqual(repository.requirementsCallsCount, 1)
let actual = repository.requirementsInvocations[0]!.username
XCTAssertEqual(given, actual)
}
}
围绕Teller编写集成测试
集成测试是确保您的代码中许多移动部件协同工作正确的好方法。Teller提供简单的方法让您能够在应用中使用Teller编写集成测试。
使用Teller进行测试的思路是给Teller一个预定义的状态。也许您需要编写一个在应用首次启动时的集成测试。也许您需要编写一个Teller已经获取到的缓存存在的测试用例。让我们看看在测试中是如何做到这一点的。
- 总是使用
setup()
测试函数清除Teller
import XCTest
import Teller
class YourIntegrationTests: XCTestCase {
override func setUp() {
Teller.shared.clear()
}
}
- 在测试函数中,给出Teller的初始状态
import XCTest
import Teller
class YourIntegrationTests: XCTestCase {
private var dataSource: RepositoryDataSource<String, RepositoryRequirements, String>!
private var repository: TellerRepository<RepositoryDataSource<String, RepositoryRequirements, String>>!
override func setUp() {
dataSource = RepositoryDataSource()
repository = TellerRepository(dataSource: dataSource)
Teller.shared.clear()
}
func test_tellerNoCach() {
let requirements = RepositoryDataSource.Requirements(username: "")
_ = TellerRepository.testing.initState(repository: repository, requirements: requirements) {
$0.noCache()
}
// Teller is all setup! When your app's code uses the `repository`, it will behave just like the `TellerRepository` has never fetched a cache successfully before.
// Write the remainder of your integration test function here.
}
func test_tellerCacheEmpty() {
let requirements = RepositoryDataSource.Requirements(username: "")
_ = TellerRepository.testing.initState(repository: repository, requirements: requirements) {
$0.cacheEmpty() {
$0.cacheTooOld()
}
}
// Teller is all setup! When your app's code uses the `repository`, it will behave just like the `TellerRepository` has fetched a cache successfully, the cache is empty, and the cache is too old which means Teller will attempt to fetch a fresh cache the next time the `TellerRepository` runs.
// There are other options for `$0.cacheEmpty()` such as:
// $0.cacheEmpty() {
// $0.cacheNotTooOld()
// }
//
// $0.cacheEmpty() {
// $0.lastFetched(Date.yesterday)
// }
// Write the remainder of your integration test function here.
}
func test_tellerCacheNotEmpty() {
let requirements = RepositoryDataSource.Requirements(username: "")
let existingCache = "existing-cache"
_ = TellerRepository.testing.initState(repository: repository, requirements: requirements) {
$0.cache(existingCache) {
$0.cacheNotTooOld()
}
}
// *Note: If your `DataSource.saveCache()` function needs to be executed on a background thread, use `TellerRepository.testing.initStateAsync()` instead of `initState()` shown here. `initSync()` runs the `DataSource.saveCache()` on the thread that you call `initState()` on.*
// Teller is all setup! When your app's code uses the `repository`, it will behave just like the `TellerRepository` has fetched a cache successfully, the cache is not empty and contains "existing-cache", and the cache is not too old (the default behavior when a state is not given) which means Teller will not attempt to fetch a fresh cache the next time the `TellerRepository` runs.
// There are other options for `$0.cache(existingCache)`. They are the same options as $0.cacheEmpty() described above.
// Write the remainder of your integration test function here.
}
}
示例应用
但是,如果您查看目录:Example/Teller/
,您将看到一个功能齐全的iOS应用,其中包含您可以使用来了解如何使用Teller、学习最佳实践并将其在XCode中编译的代码片段。
文档
文档即将到来。这个README就是迄今为止创建的所有文档。
如果您已经阅读了README文件,但仍有问题,请创建一个问题。我会回答您的问题,并在未来更新README文档,以便帮助其他人。
开发
Teller是一个简单的CocoaPods XCode工作空间。请按照下面说明进行操作以获得最佳开发体验。
- 安装cocoapods/gems并设置工作空间
$> bundle install
$> cd Example/; pod install; cd ..;
$> ./hooks/autohook.sh install # installs git hooks
作者
贡献
Teller欢迎pull requests。查看问题列表,我正在计划根据这些问题进行工作。如果您希望以这种方式做出贡献,请查看。
想要为Teller添加功能吗?在决定花费大量时间去添加函数到库中之前,请[创建一个问题](https://github.com/levibostian/Teller-iOS/issues/new),说明您想要添加的内容。这可能帮您节省时间,以防您的目的不适合Teller的用例。
名字的由来?
这个库是一个功能强大的仓库。Repository设计模式在MVVM和MVI模式中被普遍使用。Sorted相当于银行。一位银行柜员是管理您在银行中的资金并触发交易的人。因此,由于这个库促进了交易,因此teller是合适的。