Merge pull request #429 from omnivore-app/chore/swift-async

Convert ViewerPublisher to async fetchViewer
This commit is contained in:
Satindar Dhillon
2022-04-14 16:45:19 -07:00
committed by GitHub
8 changed files with 131 additions and 129 deletions

View File

@ -80,13 +80,14 @@ final class ShareExtensionViewModel: ObservableObject {
.store(in: &subscriptions)
// Using viewerPublisher to get fast feedback for auth/network errors
services.dataService.viewerPublisher()
.sink { [weak self] completion in
guard case let .failure(error) = completion else { return }
self?.debugText = "saveArticleError: \(error)"
self?.status = .failed(error: .unknown(description: ""))
} receiveValue: { _ in }
.store(in: &subscriptions)
Task {
do {
_ = try await services.dataService.fetchViewer()
} catch {
debugText = "saveArticleError: \(error)"
status = .failed(error: .unknown(description: ""))
}
}
}
}

View File

@ -5,7 +5,7 @@ import SwiftUI
import Utils
import Views
final class HomeFeedViewModel: ObservableObject {
@MainActor final class HomeFeedViewModel: ObservableObject {
var currentDetailViewModel: LinkItemDetailViewModel?
/// Track progress updates to be committed when user navigates back to grid view
@ -43,7 +43,7 @@ final class HomeFeedViewModel: ObservableObject {
// Check if user has scrolled to the last five items in the list
if let itemIndex = itemIndex, itemIndex > thresholdIndex, items.count < thresholdIndex + 10 {
loadItems(dataService: dataService, isRefresh: false)
Task { await loadItems(dataService: dataService, isRefresh: false) }
}
}
@ -61,12 +61,9 @@ final class HomeFeedViewModel: ObservableObject {
isLoading = true
// Cache the viewer
if dataService.currentViewer == nil {
dataService.viewerPublisher().sink(
receiveCompletion: { _ in },
receiveValue: { _ in }
)
.store(in: &subscriptions)
Task { _ = try? await dataService.fetchViewer() }
}
dataService.libraryItemsPublisher(

View File

@ -9,7 +9,7 @@ enum PDFProvider {
static var pdfViewerProvider: ((URL, FeedItem) -> AnyView)?
}
final class LinkItemDetailViewModel: ObservableObject {
@MainActor final class LinkItemDetailViewModel: ObservableObject {
let homeFeedViewModel: HomeFeedViewModel
@Published var item: FeedItem
@Published var webAppWrapperViewModel: WebAppWrapperViewModel?
@ -45,31 +45,22 @@ final class LinkItemDetailViewModel: ObservableObject {
.store(in: &subscriptions)
}
func loadWebAppWrapper(dataService: DataService, rawAuthCookie: String?) {
// Attempt to get `Viewer` from DataService
if let currentViewer = dataService.currentViewer {
func loadWebAppWrapper(dataService: DataService, rawAuthCookie: String?) async {
let viewer: Viewer? = await {
if let currentViewer = dataService.currentViewer {
return currentViewer
}
return try? await dataService.fetchViewer()
}()
if let viewer = viewer {
createWebAppWrapperViewModel(
username: currentViewer.username,
username: viewer.username,
dataService: dataService,
rawAuthCookie: rawAuthCookie
)
return
}
dataService.viewerPublisher().sink(
receiveCompletion: { completion in
guard case let .failure(error) = completion else { return }
print(error)
},
receiveValue: { [weak self] viewer in
self?.createWebAppWrapperViewModel(
username: viewer.username,
dataService: dataService,
rawAuthCookie: rawAuthCookie
)
}
)
.store(in: &subscriptions)
}
private func createWebAppWrapperViewModel(username: String, dataService: DataService, rawAuthCookie: String?) {
@ -265,8 +256,8 @@ struct LinkItemDetailView: View {
navBar
Spacer()
}
.onAppear {
viewModel.loadWebAppWrapper(
.task {
await viewModel.loadWebAppWrapper(
dataService: dataService,
rawAuthCookie: authenticator.omnivoreAuthCookieString
)
@ -311,8 +302,8 @@ struct LinkItemDetailView: View {
Text("Loading...")
Spacer()
}
.onAppear {
viewModel.loadWebAppWrapper(
.task {
await viewModel.loadWebAppWrapper(
dataService: dataService,
rawAuthCookie: authenticator.omnivoreAuthCookieString
)

View File

@ -5,12 +5,10 @@ import SwiftUI
import Utils
import Views
final class ProfileContainerViewModel: ObservableObject {
@MainActor final class ProfileContainerViewModel: ObservableObject {
@Published var isLoading = false
@Published var profileCardData = ProfileCardData()
var subscriptions = Set<AnyCancellable>()
var appVersionString: String {
if let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
return "Omnivore Version \(appVersion)"
@ -19,18 +17,14 @@ final class ProfileContainerViewModel: ObservableObject {
}
}
func loadProfileData(dataService: DataService) {
dataService.viewerPublisher().sink(
receiveCompletion: { _ in },
receiveValue: { [weak self] viewer in
self?.profileCardData = ProfileCardData(
name: viewer.name,
username: viewer.username,
imageURL: viewer.profileImageURL.flatMap { URL(string: $0) }
)
}
func loadProfileData(dataService: DataService) async {
guard let viewer = try? await dataService.fetchViewer() else { return }
profileCardData = ProfileCardData(
name: viewer.name,
username: viewer.username,
imageURL: viewer.profileImageURL.flatMap { URL(string: $0) }
)
.store(in: &subscriptions)
}
}
@ -59,7 +53,9 @@ struct ProfileView: View {
Group {
Section {
ProfileCard(data: viewModel.profileCardData)
.onAppear { viewModel.loadProfileData(dataService: dataService) }
.task {
await viewModel.loadProfileData(dataService: dataService)
}
}
Section {

View File

@ -91,10 +91,10 @@ struct InnerRootView: View {
if viewModel.webLinkPath != nil {
viewModel.webLinkPath = nil
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
viewModel.onOpenURL(url: url)
Task { await viewModel.onOpenURL(url: url) }
}
} else {
viewModel.onOpenURL(url: url)
Task { await viewModel.onOpenURL(url: url) }
}
}
}

View File

@ -20,8 +20,6 @@ public final class RootViewModel: ObservableObject {
@Published var snackbarMessage: String?
@Published var showSnackbar = false
public var subscriptions = Set<AnyCancellable>()
public init() {
registerFonts()
@ -57,7 +55,7 @@ public final class RootViewModel: ObservableObject {
)
}
func onOpenURL(url: URL) {
@MainActor func onOpenURL(url: URL) async {
guard let linkRequestID = DeepLink.make(from: url)?.linkRequestID else { return }
if let username = services.dataService.currentViewer?.username {
@ -66,17 +64,10 @@ public final class RootViewModel: ObservableObject {
return
}
services.dataService.viewerPublisher().sink(
receiveCompletion: { completion in
guard case let .failure(error) = completion else { return }
print(error)
},
receiveValue: { [weak self] viewer in
let path = self?.linkRequestPath(username: viewer.username, requestID: linkRequestID) ?? ""
self?.webLinkPath = SafariWebLinkPath(id: UUID(), path: path)
}
)
.store(in: &subscriptions)
if let viewer = try? await services.dataService.fetchViewer() {
let path = linkRequestPath(username: viewer.username, requestID: linkRequestID)
webLinkPath = SafariWebLinkPath(id: UUID(), path: path)
}
}
func triggerPushNotificationRequestIfNeeded() {

View File

@ -0,0 +1,85 @@
import Combine
import Foundation
import Models
import SwiftGraphQL
import Utils
public extension DataService {
func fetchViewer() async throws -> Viewer {
let selection = Selection<Viewer, Objects.User> {
Viewer(
username: try $0.profile(
selection: .init { try $0.username() }
),
name: try $0.name(),
profileImageURL: try $0.profile(
selection: .init { try $0.pictureUrl() }
),
userID: try $0.id()
)
}
let query = Selection.Query {
try $0.me(selection: selection.nonNullOrFail)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
return try await withCheckedThrowingContinuation { continuation in
send(query, to: path, headers: headers) { [weak self] result in
switch result {
case let .success(payload):
self?.currentViewer = payload.data
if UserDefaults.standard.string(forKey: Keys.userIdKey) == nil {
UserDefaults.standard.setValue(payload.data.userID, forKey: Keys.userIdKey)
DataService.registerIntercomUser?(payload.data.userID)
}
continuation.resume(returning: payload.data)
case .failure:
continuation.resume(throwing: BasicError.message(messageText: "http error"))
}
}
}
}
}
extension DataService {
@available(*, deprecated, message: "use async version instead")
func internalViewerPublisher() -> AnyPublisher<Viewer, BasicError> {
let selection = Selection<Viewer, Objects.User> {
Viewer(
username: try $0.profile(
selection: .init { try $0.username() }
),
name: try $0.name(),
profileImageURL: try $0.profile(
selection: .init { try $0.pictureUrl() }
),
userID: try $0.id()
)
}
let query = Selection.Query {
try $0.me(selection: selection.nonNullOrFail)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
return Deferred {
Future { [weak self] promise in
send(query, to: path, headers: headers) { result in
switch result {
case let .success(payload):
self?.currentViewer = payload.data
promise(.success(payload.data))
case .failure:
promise(.failure(.message(messageText: "http error")))
}
}
}
}
.eraseToAnyPublisher()
}
}

View File

@ -1,59 +0,0 @@
import Combine
import Foundation
import Models
import SwiftGraphQL
import Utils
public extension DataService {
func viewerPublisher() -> AnyPublisher<Viewer, BasicError> {
internalViewerPublisher()
.handleEvents(receiveOutput: {
// Persist ID so AppDelegate can use it to register Intercom users at launch time
if UserDefaults.standard.string(forKey: Keys.userIdKey) == nil {
UserDefaults.standard.setValue($0.userID, forKey: Keys.userIdKey)
DataService.registerIntercomUser?($0.userID)
}
})
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
extension DataService {
func internalViewerPublisher() -> AnyPublisher<Viewer, BasicError> {
let selection = Selection<Viewer, Objects.User> {
Viewer(
username: try $0.profile(
selection: .init { try $0.username() }
),
name: try $0.name(),
profileImageURL: try $0.profile(
selection: .init { try $0.pictureUrl() }
),
userID: try $0.id()
)
}
let query = Selection.Query {
try $0.me(selection: selection.nonNullOrFail)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
return Deferred {
Future { [weak self] promise in
send(query, to: path, headers: headers) { result in
switch result {
case let .success(payload):
self?.currentViewer = payload.data
promise(.success(payload.data))
case .failure:
promise(.failure(.message(messageText: "http error")))
}
}
}
}
.eraseToAnyPublisher()
}
}