Merge pull request #1 from knezzy/master

Added user data store as an example of ObservableObject.
This commit is contained in:
William Brawner 2019-09-29 17:21:56 -07:00 committed by GitHub
commit 8a56f7bd2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 94 additions and 51 deletions

View file

@ -22,6 +22,7 @@
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC9528233C433400BFB70A /* TransactionRepository.swift */; };
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC952B233C434800BFB70A /* UserRepository.swift */; };
28AC952E233C43A300BFB70A /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC952D233C43A300BFB70A /* User.swift */; };
543ECE42233E82A40018A9D9 /* UserDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543ECE41233E82A40018A9D9 /* UserDataStore.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -65,6 +66,7 @@
28AC9528233C433400BFB70A /* TransactionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionRepository.swift; sourceTree = "<group>"; };
28AC952B233C434800BFB70A /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = "<group>"; };
28AC952D233C43A300BFB70A /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
543ECE41233E82A40018A9D9 /* UserDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataStore.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -187,6 +189,7 @@
28AC952B233C434800BFB70A /* UserRepository.swift */,
28AC952D233C43A300BFB70A /* User.swift */,
2857EAEE233DA73B0026BC83 /* UserData.swift */,
543ECE41233E82A40018A9D9 /* UserDataStore.swift */,
);
path = User;
sourceTree = "<group>";
@ -332,6 +335,7 @@
28AC9525233C42D100BFB70A /* BudgetApiService.swift in Sources */,
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */,
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */,
543ECE42233E82A40018A9D9 /* UserDataStore.swift in Sources */,
28AC952E233C43A300BFB70A /* User.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View file

@ -16,7 +16,7 @@ class BudgetApiService {
self.requestHelper = requestHelper
}
func login(username: String, password: String) -> Future<User, NetworkError> {
func login(username: String, password: String) -> AnyPublisher<User, NetworkError> {
requestHelper.credentials = (username, password)
return requestHelper.post(
endPoint: "/users/login",
@ -24,11 +24,11 @@ class BudgetApiService {
)
}
func getUser(id: Int) -> Future<User, NetworkError> {
func getUser(id: Int) -> AnyPublisher<User, NetworkError> {
return requestHelper.get(endPoint: "/users/\(id)")
}
func searchUsers(query: String) -> Future<[User], NetworkError> {
func searchUsers(query: String) -> AnyPublisher<[User], NetworkError> {
return requestHelper.get(
endPoint: "/users/search",
queries: ["query": [query]]
@ -40,6 +40,8 @@ class RequestHelper {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let baseUrl: String
// Note:
// There shouldn't be a reason to sink when building a typical request.
private var subscriptions = Set<AnyCancellable>()
var credentials: (String, String)?
@ -50,7 +52,7 @@ class RequestHelper {
func get<ResultType: Codable>(
endPoint: String,
queries: [String: Array<String>]? = nil
) -> Future<ResultType, NetworkError> {
) -> AnyPublisher<ResultType, NetworkError> {
var combinedEndPoint = endPoint
if (queries != nil) {
for (key, values) in queries! {
@ -67,7 +69,7 @@ class RequestHelper {
func post<ResultType: Codable>(
endPoint: String,
data: Codable
) -> Future<ResultType, NetworkError> {
) -> AnyPublisher<ResultType, NetworkError> {
return buildRequest(
endPoint: endPoint,
method: "POST",
@ -78,7 +80,7 @@ class RequestHelper {
func put<ResultType: Codable>(
endPoint: String,
data: ResultType
) -> Future<ResultType, NetworkError> {
) -> AnyPublisher<ResultType, NetworkError> {
return buildRequest(
endPoint: endPoint,
method: "PUT",
@ -86,7 +88,7 @@ class RequestHelper {
)
}
func delete<ResultType: Codable>(endPoint: String) -> Future<ResultType, NetworkError> {
func delete<ResultType: Codable>(endPoint: String) -> AnyPublisher<ResultType, NetworkError> {
return buildRequest(endPoint: endPoint, method: "DELETE")
}
@ -94,48 +96,42 @@ class RequestHelper {
endPoint: String,
method: String,
data: Encodable? = nil
) -> Future<ResultType, NetworkError> {
return Future<ResultType, NetworkError> {[unowned self] promise in
guard let url = URL(string: self.baseUrl + endPoint) else {
promise(.failure(NetworkError.invalidUrl))
return
) -> AnyPublisher<ResultType, NetworkError> {
guard let url = URL(string: self.baseUrl + endPoint) else {
return Future<ResultType, NetworkError> { promise in
promise(.failure(.invalidUrl))
}.eraseToAnyPublisher()
}
var request = URLRequest(url: url)
if (self.credentials != nil) {
if let encodedCredentials = "\(self.credentials!.0):\(self.credentials!.1)"
.data(using: String.Encoding.utf8)?.base64EncodedString() {
request.addValue("Basic \(encodedCredentials)", forHTTPHeaderField: "Authorization")
}
}
request.httpBody = data?.toJSONData()
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = method
let task = URLSession.shared.dataTaskPublisher(for: request)
.tryMap { (data, res) -> Data in
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
switch (res as? HTTPURLResponse)?.statusCode {
case 400: throw NetworkError.badRequest
case 401, 403: throw NetworkError.unauthorized
case 404: throw NetworkError.notFound
default: throw NetworkError.unknown
}
var request = URLRequest(url: url)
if (self.credentials != nil) {
if let encodedCredentials = "\(self.credentials!.0):\(self.credentials!.1)"
.data(using: String.Encoding.utf8)?.base64EncodedString() {
request.addValue("Basic \(encodedCredentials)", forHTTPHeaderField: "Authorization")
}
}
request.httpBody = data?.toJSONData()
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = method
// TODO: Return this as well?
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { (data, res) -> Data in
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
switch (res as? HTTPURLResponse)?.statusCode {
case 400: throw NetworkError.badRequest
case 401, 403: throw NetworkError.unauthorized
case 404: throw NetworkError.notFound
default: throw NetworkError.unknown
}
}
if let jsonResult = try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary {
print(jsonResult)
}
return data
}
.decode(type: ResultType.self, decoder: JSONDecoder())
.print()
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { data in
promise(.success(data))
})
.store(in: &self.subscriptions)
}
}
return data
}
.decode(type: ResultType.self, decoder: JSONDecoder())
.mapError {
return NetworkError.jsonParsingFailed($0)
}
return task.eraseToAnyPublisher()
}
}
@ -145,6 +141,7 @@ enum NetworkError: Error {
case unauthorized
case badRequest
case invalidUrl
case jsonParsingFailed(Error)
}
extension Encodable {

View file

@ -0,0 +1,42 @@
import Foundation
import Combine
class UserDataStore: ObservableObject {
// Note: You can combine these into one Result type
// Result<User, Status>
var currentUser: User? = nil
var status: UserStatus = .unauthenticated
func login(username: String, password: String) {
// Changes the status and notifies any observers of the change
self.status = .authenticating
self.objectWillChange.send()
// Perform the login
_ = self.userRepository.login(username: username, password: password)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (status) in
switch status {
case .finished:
return
// Do nothing it means the network request just ended
case .failure(let networkError):
// Poulate your status with failed authenticating
self.status = .failedAuthentication
self.objectWillChange.send()
}
}) { (user) in
self.currentUser = user
self.objectWillChange.send()
}
}
init(_ userRepository: UserRepository) {
self.userRepository = userRepository
}
private let userRepository: UserRepository
}

View file

@ -16,15 +16,15 @@ class UserRepository {
self.apiService = apiService
}
func getUser(id: Int) -> Future<User, NetworkError> {
func getUser(id: Int) -> AnyPublisher<User, NetworkError> {
return apiService.getUser(id: id)
}
func searchUsers(withUsername: String) -> Future<[User], NetworkError> {
func searchUsers(withUsername: String) -> AnyPublisher<[User], NetworkError> {
return apiService.searchUsers(query: withUsername)
}
func login(username: String, password: String) -> Future<User, NetworkError> {
func login(username: String, password: String) -> AnyPublisher<User, NetworkError> {
return apiService.login(username: username, password: password)
}
}