Skip to content

Commit 818aa1c

Browse files
authored
Merge pull request #4 from Lickability/new-example-app
Adds New Example Project
2 parents b88960b + 16e1a5e commit 818aa1c

20 files changed

Lines changed: 998 additions & 216 deletions

Example/API/APIRequest.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// APIRequest.swift
3+
// ViewStore
4+
//
5+
// Created by Twig on 2/24/22.
6+
//
7+
8+
import Foundation
9+
import Provider
10+
11+
/// Represents the requests we can make for content displayed in the app.
12+
enum APIRequest: ProviderRequest {
13+
14+
/// Fetches albums of placeholder photo models.
15+
case photos
16+
17+
// MARK: - ProviderRequest
18+
19+
var persistenceKey: Key? {
20+
switch self {
21+
case .photos: return "Photos"
22+
}
23+
}
24+
25+
// MARK: - NetworkRequest
26+
27+
var path: String {
28+
switch self {
29+
case .photos:
30+
return "/photos"
31+
}
32+
}
33+
34+
var queryParameters: [URLQueryItem] {
35+
return [URLQueryItem(name: "_limit", value: "5")]
36+
}
37+
38+
var baseURL: URL {
39+
return URL(string: "https://jsonplaceholder.typicode.com")!
40+
}
41+
}
42+

Example/Array+Filtering.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// Array+Filtering.swift
3+
// ViewStore
4+
//
5+
// Created by Michael Liberatore on 7/13/22.
6+
//
7+
8+
import Foundation
9+
10+
extension Array where Element == Photo {
11+
12+
/// Filters an array of photos by `searchText` returning only the photos that contain `searchText` in the photo `title` (case insensitive).
13+
/// - Parameter searchText: The text to query titles for.
14+
func filter(searchText: String) -> [Photo] {
15+
guard !searchText.isEmpty else { return self }
16+
return filter { photo in
17+
photo.title.localizedCaseInsensitiveContains(searchText)
18+
}
19+
}
20+
}

Example/Example.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// Example.swift
3+
// ViewStore
4+
//
5+
// Created by Twig on 2/24/22.
6+
//
7+
8+
import SwiftUI
9+
import Provider
10+
import Networking
11+
import Persister
12+
13+
/// The entry point into the example app.
14+
@main
15+
struct Example: App {
16+
17+
@State private var photoProvider: ItemProvider = {
18+
let controller = NetworkController()
19+
let diskCache = DiskCache(rootDirectoryURL: FileManager.default.applicationSupportDirectoryURL.appendingPathComponent("Photos"))
20+
let cache = MemoryCache(capacity: .unlimited, expirationPolicy: .never)
21+
let persister = Persister(memoryCache: cache, diskCache: diskCache)
22+
23+
return ItemProvider(networkRequestPerformer: controller, cache: persister)
24+
}()
25+
26+
// MARK: - App
27+
28+
var body: some Scene {
29+
WindowGroup {
30+
TabView {
31+
PhotoListOriginal(provider: photoProvider)
32+
.tabItem {
33+
Image(systemName: "photo")
34+
Text("Photos (Original)")
35+
}
36+
37+
PhotoList(provider: photoProvider)
38+
.tabItem {
39+
Image(systemName: "photo")
40+
Text("Photos")
41+
}
42+
}
43+
}
44+
}
45+
}

Example/ExampleApp.swift

Lines changed: 0 additions & 17 deletions
This file was deleted.

Example/ExampleView.swift

Lines changed: 0 additions & 34 deletions
This file was deleted.

Example/ExampleViewStore.swift

Lines changed: 0 additions & 57 deletions
This file was deleted.

Example/MockItemProvider.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//
2+
// MockItemProvider.swift
3+
// ViewStore
4+
//
5+
// Created by Michael Liberatore on 7/11/22.
6+
//
7+
8+
import Foundation
9+
import Combine
10+
import Provider
11+
import Persister
12+
import Networking
13+
14+
/// A provider meant to be usable by SwiftUI previews and unit tests to provide mocked, successful `Photo`s synchronously.
15+
final class MockItemProvider: Provider {
16+
private let photos: [Photo]
17+
18+
/// Creates a new `MockItemProvider` with the specified `Photo`s.
19+
/// - Parameter photos: the `Photo`s that the provider will "retrieve" synchronously.
20+
init(photos: [Photo]) {
21+
self.photos = photos
22+
}
23+
24+
/// Creates a new `MockItemProvider` by generating a `Photo` for each `Int` within the range of `(1...photosCount)`.
25+
/// - Parameter photosCount: The number of photos to generate. Note that the bundle must contain images named with the pattern "thumbnail-x.png" where x can be all values between `1` and `photosCount` (inclusive).
26+
init(photosCount: Int) {
27+
self.photos = (1...photosCount).map { index in
28+
let url = Bundle.main.url(forResource: "thumbnail-\(index)", withExtension: "png")!
29+
return Photo(albumId: 0, id: index, title: "Hello-\(index)", url: url, thumbnailUrl: url)
30+
}
31+
}
32+
33+
// MARK: - Provider
34+
35+
func provide<Item>(request: ProviderRequest, decoder: ItemDecoder, providerBehaviors: [ProviderBehavior], requestBehaviors: [RequestBehavior], handlerQueue: DispatchQueue, allowExpiredItem: Bool, itemHandler: @escaping (Result<Item, ProviderError>) -> Void) where Item : Identifiable, Item : Decodable, Item : Encodable {
36+
itemHandler((photos.first as? Item).flatMap { .success($0) } ?? .failure(.networkError(.noData)))
37+
}
38+
39+
func provideItems<Item>(request: ProviderRequest, decoder: ItemDecoder, providerBehaviors: [ProviderBehavior], requestBehaviors: [RequestBehavior], handlerQueue: DispatchQueue, allowExpiredItems: Bool, itemsHandler: @escaping (Result<[Item], ProviderError>) -> Void) where Item : Identifiable, Item : Decodable, Item : Encodable {
40+
itemsHandler((photos as? [Item]).flatMap { .success($0) } ?? .failure(.networkError(.noData)))
41+
}
42+
43+
func provide<Item>(request: ProviderRequest, decoder: ItemDecoder, providerBehaviors: [ProviderBehavior], requestBehaviors: [RequestBehavior], allowExpiredItem: Bool) -> AnyPublisher<Item, ProviderError> where Item : Identifiable, Item : Decodable, Item : Encodable {
44+
if let item = photos.first as? Item {
45+
return Just(item)
46+
.setFailureType(to: ProviderError.self)
47+
.eraseToAnyPublisher()
48+
} else {
49+
return Fail(error: ProviderError.networkError(.noData))
50+
.eraseToAnyPublisher()
51+
}
52+
}
53+
54+
func provideItems<Item>(request: ProviderRequest, decoder: ItemDecoder, providerBehaviors: [ProviderBehavior], requestBehaviors: [RequestBehavior], allowExpiredItems: Bool) -> AnyPublisher<[Item], ProviderError> where Item : Identifiable, Item : Decodable, Item : Encodable {
55+
if let items = photos as? [Item] {
56+
return Just(items)
57+
.setFailureType(to: ProviderError.self)
58+
.eraseToAnyPublisher()
59+
} else {
60+
return Fail(error: ProviderError.networkError(.noData))
61+
.eraseToAnyPublisher()
62+
}
63+
}
64+
}

Example/Photos/Photo.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// Photo.swift
3+
// ViewStore
4+
//
5+
// Created by Twig on 2/24/22.
6+
//
7+
8+
import Foundation
9+
import Provider
10+
11+
/// Represents a remote image.
12+
struct Photo: Codable, Identifiable, Swift.Identifiable {
13+
14+
/// The unique identifier associated with the album to which this photo belongs.
15+
let albumId: Int
16+
17+
/// The unique identifier of this photo.
18+
let id: Int
19+
20+
/// Descriptive text that is associated with the image, i.e. what it is called.
21+
let title: String
22+
23+
/// The URL at which the full image data can be retrieved.
24+
let url: URL
25+
26+
/// THe URL at which a lower resolution version of the image data can be retrieved.
27+
let thumbnailUrl: URL
28+
29+
// MARK: - Identifiable
30+
31+
var identifier: Key {
32+
return "\(id)"
33+
}
34+
}

Example/Photos/PhotoList.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// PhotoList.swift
3+
// ViewStore
4+
//
5+
// Created by Twig on 2/24/22.
6+
//
7+
8+
import SwiftUI
9+
import Provider
10+
11+
/// Displays a list of photos retrieved from an API. Uses a `ViewStore` for coordination with the data source.
12+
struct PhotoList: View {
13+
14+
@StateObject private var store: PhotoListViewStore
15+
16+
/// Creates a new `PhotoList`.
17+
/// - Parameters:
18+
/// - provider: The provider responsible for fetching photos.
19+
/// - scheduler: Determines how state updates are scheduled to be delivered in the view store. Defaults to `default`, which asynchronously schedules updates on the main queue.
20+
init(provider: Provider, scheduler: MainQueueScheduler = .init(type: .default)) {
21+
self._store = StateObject(wrappedValue: PhotoListViewStore(provider: provider, scheduler: scheduler))
22+
}
23+
24+
// MARK: - View
25+
26+
var body: some View {
27+
NavigationView {
28+
ZStack {
29+
switch store.viewState.status {
30+
case .loading:
31+
ProgressView()
32+
.progressViewStyle(.circular)
33+
.scaleEffect(x: 2, y: 2)
34+
case let .content(photos):
35+
List {
36+
Section {
37+
ForEach(photos) { photo in
38+
HStack {
39+
AsyncImage(url: photo.thumbnailUrl) { image in
40+
image.resizable()
41+
.aspectRatio(contentMode: .fit)
42+
} placeholder: {
43+
ProgressView()
44+
}
45+
.frame(width: 150, height: 150)
46+
47+
Text(photo.title)
48+
}
49+
}
50+
} header: {
51+
Toggle("Show Count", isOn: store.showsPhotoCount)
52+
.animation(.easeInOut, value: store.viewState.showsPhotoCount)
53+
}
54+
}
55+
case let .error(error):
56+
VStack {
57+
Image(systemName: "xmark.octagon")
58+
Text(error.localizedDescription)
59+
}
60+
}
61+
62+
}
63+
.navigationBarTitleDisplayMode(.inline)
64+
.navigationTitle(store.viewState.navigationTitle)
65+
.searchable(text: store.searchText, placement: .navigationBarDrawer(displayMode: .always))
66+
}
67+
}
68+
}
69+
70+
struct PhotoList_Previews: PreviewProvider {
71+
static var previews: some View {
72+
PhotoList(provider: MockItemProvider(photosCount: 3), scheduler: .init(type: .synchronous))
73+
}
74+
}

0 commit comments

Comments
 (0)