diff --git a/MySalesforceAccounts.xcodeproj/project.pbxproj b/MySalesforceAccounts.xcodeproj/project.pbxproj index 8d4925d..1f3ac56 100644 --- a/MySalesforceAccounts.xcodeproj/project.pbxproj +++ b/MySalesforceAccounts.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ EA6351F32666947600D3F4C1 /* Salesforce.json in Resources */ = {isa = PBXBuildFile; fileRef = EA6351F22666947600D3F4C1 /* Salesforce.json */; }; EAEE128427F0DB9100BC3D2E /* SwiftlySalesforce in Frameworks */ = {isa = PBXBuildFile; productRef = EAEE128327F0DB9100BC3D2E /* SwiftlySalesforce */; }; EAEE128627F0F26300BC3D2E /* LoadingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEE128527F0F26300BC3D2E /* LoadingState.swift */; }; + EAEE128827F0FDC600BC3D2E /* AsyncButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEE128727F0FDC600BC3D2E /* AsyncButton.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -52,6 +53,7 @@ EA6351E0266693BC00D3F4C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EA6351F22666947600D3F4C1 /* Salesforce.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Salesforce.json; sourceTree = ""; }; EAEE128527F0F26300BC3D2E /* LoadingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingState.swift; sourceTree = ""; }; + EAEE128727F0FDC600BC3D2E /* AsyncButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncButton.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -104,6 +106,7 @@ isa = PBXGroup; children = ( EA6351C5266693BC00D3F4C1 /* Assets.xcassets */, + EAEE128727F0FDC600BC3D2E /* AsyncButton.swift */, EA6351C3266693BB00D3F4C1 /* ContentView.swift */, EA49FFE52666E0B200917E3D /* ErrorView.swift */, EA6351CA266693BC00D3F4C1 /* Info.plist */, @@ -278,6 +281,7 @@ buildActionMask = 2147483647; files = ( EAEE128627F0F26300BC3D2E /* LoadingState.swift in Sources */, + EAEE128827F0FDC600BC3D2E /* AsyncButton.swift in Sources */, EA6351C4266693BB00D3F4C1 /* ContentView.swift in Sources */, EA6351C2266693BB00D3F4C1 /* MySalesforceAccountsApp.swift in Sources */, EA49FFE62666E0B200917E3D /* ErrorView.swift in Sources */, diff --git a/MySalesforceAccounts/AsyncButton.swift b/MySalesforceAccounts/AsyncButton.swift new file mode 100644 index 0000000..bd0864c --- /dev/null +++ b/MySalesforceAccounts/AsyncButton.swift @@ -0,0 +1,84 @@ +// +// AsyncButton.swift +// MySalesforceAccounts +// +// Created by Michael Epstein on 3/27/22. +// + +import Foundation +import SwiftUI + +// Borrowed from https://www.swiftbysundell.com/articles/building-an-async-swiftui-button/ +struct AsyncButton: View { + + var action: () async -> Void + var actionOptions = Set(ActionOption.allCases) + @ViewBuilder var label: () -> Label + + @State private var isDisabled = false + @State private var showProgressView = false + + var body: some View { + Button( + action: { + if actionOptions.contains(.disableButton) { + isDisabled = true + } + + Task { + var progressViewTask: Task? + + if actionOptions.contains(.showProgressView) { + progressViewTask = Task { + try await Task.sleep(nanoseconds: 150_000_000) + showProgressView = true + } + } + + await action() + progressViewTask?.cancel() + + isDisabled = false + showProgressView = false + } + }, + label: { + ZStack { + label().opacity(showProgressView ? 0 : 1) + + if showProgressView { + ProgressView() + } + } + } + ) + .disabled(isDisabled) + } +} + +extension AsyncButton { + enum ActionOption: CaseIterable { + case disableButton + case showProgressView + } +} + +extension AsyncButton where Label == Text { + init(_ label: String, + actionOptions: Set = Set(ActionOption.allCases), + action: @escaping () async -> Void) { + self.init(action: action) { + Text(label) + } + } +} + +extension AsyncButton where Label == Image { + init(systemImageName: String, + actionOptions: Set = Set(ActionOption.allCases), + action: @escaping () async -> Void) { + self.init(action: action) { + Image(systemName: systemImageName) + } + } +} diff --git a/MySalesforceAccounts/ContentView.swift b/MySalesforceAccounts/ContentView.swift index 197817b..23ad76e 100644 --- a/MySalesforceAccounts/ContentView.swift +++ b/MySalesforceAccounts/ContentView.swift @@ -12,37 +12,37 @@ import SwiftlySalesforce struct ContentView: View { @StateObject var salesforce: Connection = try! Salesforce.connect() - @State var state: LoadingState> = .idle + @State var state: LoadingState<[SalesforceRecord]> = .idle var body: some View { switch state { case .idle: Color.clear .task { - do { - let results = try await salesforce.myRecords(type: "Account", fields: ["Id", "Name", "BillingCity"]) - state = .loaded(results) - } - catch { - state = .failed(error) - } + await load() } case .loading: ProgressView() case .failed(let error): - ErrorView(error: error, retry: { print("TODO") }) - case .loaded(let queryResult): - QueryResultView(queryResult: queryResult) + ErrorView(error: error, retry: { await load() }) + case .loaded(let records): + AsyncButton(action: load) { + HStack { + AsyncButton(systemImageName: "arrow.clockwise", action: load) + Text("Loaded: \(Date().formatted(date: .abbreviated, time: .standard))") + } + } + RecordsView(records: records) } } } extension ContentView { - struct QueryResultView: View { - var queryResult: QueryResult + struct RecordsView: View { + var records: [SalesforceRecord] var body: some View { - List(queryResult.records, id: \.id) { account in + List(records, id: \.id) { account in VStack(alignment: .leading) { Text(account["Name"] ?? "N/A").bold() Text("ID: \(account.id)") @@ -53,6 +53,22 @@ extension ContentView { } } +extension ContentView { + + func load() async { + state = .loading + Task { + do { + let queryResults = try await salesforce.myRecords(type: "Account", fields: ["Id", "Name", "BillingCity"]) + state = .loaded(queryResults.records) + } + catch { + state = .failed(error) + } + } + } +} + struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() diff --git a/MySalesforceAccounts/ErrorView.swift b/MySalesforceAccounts/ErrorView.swift index a08d224..624d4cc 100644 --- a/MySalesforceAccounts/ErrorView.swift +++ b/MySalesforceAccounts/ErrorView.swift @@ -10,9 +10,9 @@ import SwiftUI struct ErrorView: View { var error: Error - var retry: () -> () + var retry: () async -> () - init(error: Error, retry: @escaping () -> ()) { + init(error: Error, retry: @escaping () async -> ()) { self.error = error self.retry = retry } @@ -27,7 +27,9 @@ struct ErrorView: View { .bold() Text(error.localizedDescription) Button("Try Again") { - retry() + Task { + await retry() + } } Spacer() }