├── .gitattributes ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ └── ErrorableView.xcscheme ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ErrorableView │ ├── Abstract │ └── ErrorableViewModifier.swift │ ├── Enums │ ├── ErrorPresentTypes.swift │ └── PageStates.swift │ └── Views │ ├── DefaultErrorView.swift │ └── DefaultLoadingView.swift └── Tests └── ErrorableViewTests └── ErrorableViewTests.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/ErrorableView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mehmet Ateş  4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ErrorableView", 8 | platforms: [ 9 | .iOS(.v15), 10 | .macOS(.v13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "ErrorableView", 16 | targets: ["ErrorableView"]), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package, defining a module or a test suite. 20 | // Targets can depend on other targets in this package and products from dependencies. 21 | .target( 22 | name: "ErrorableView"), 23 | .testTarget( 24 | name: "ErrorableViewTests", 25 | dependencies: ["ErrorableView"]), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ErrorableView-SwiftUI 2 | 3 | ## How to install this package 4 | 5 | + Open your project on Xcode 6 | + Go to the Project Tab and select "Package Dependencies" 7 | + Click "+" and search this package with use git clone URL 8 | + Don't change anything and click Add Package 9 | + The package will be attached to the targeted application 10 | 11 | ## How to use this package 12 | ### Just use The "ErrorableViewModifier" with an $pageState property 13 | ```swift 14 | struct TestView: View { 15 | @State private var pageState: PageStates = .loading 16 | 17 | var body: some View { 18 | // YOUR CODES 19 | .akErrorView(pageState: $pageState) { 20 | // TRY AGAIN ACTION 21 | } 22 | } 23 | } 24 | ``` 25 | ## Useful Tips 26 | This package allows you to manage your page error state easily. But actually, it's useful. What do you get to know? 27 | 28 | - Generic Error Page Support: 29 | The Package includes a DefaultErrorPage but you don't want to use it. Use the ErrorableView protocol, and create your error page. 30 | ```swift 31 | protocol ErrorableView: View { 32 | var type: ErrorPresentTypes { get set } 33 | } 34 | ``` 35 | This protocol only wants to create a type property for your error page presentation state. If your view comformed the protocol you'll use this modifier code block under the below. 36 | ```swift 37 | .modifier(ErrorableViewModifier(pageState: $viewModel.pageState) { // Like this 38 | YourView() { 39 | viewModel.reload() 40 | } 41 | }) 42 | ``` 43 | - Fully Customisable Error Page: 44 | The package includes a customizable ErrorPage named DefaultErrorPage. You can use that uiModel to update DefaultErrorPage. 45 | ```swift 46 | @frozen public struct DefaultErrorPageUIModel { 47 | var title: LocalizedStringKey 48 | var subtitle: LocalizedStringKey? 49 | var icon: String? 50 | var systemName: String? 51 | var buttonTitle: LocalizedStringKey? 52 | } 53 | ``` 54 | 55 | ## Examples 56 | ### Sheet Type 57 | https://github.com/devmehmetates/ErrorableView-SwiftUI/assets/74152011/1fe9e28a-8ba3-48b8-8d85-b2eb4c6aa672 58 | 59 | ### OnPage Type 60 | https://github.com/devmehmetates/ErrorableView-SwiftUI/assets/74152011/2c579c96-adec-4d6e-9739-1892d97666aa 61 | 62 | ### Fullscreen Type 63 | https://github.com/devmehmetates/ErrorableView-SwiftUI/assets/74152011/6e34332f-6c24-489d-8bd2-bfd5ab2fb027 64 | -------------------------------------------------------------------------------- /Sources/ErrorableView/Abstract/ErrorableViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mehmet Ateş on 1.03.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | @ViewBuilder 12 | func akErrorView( 13 | pageState: Binding, 14 | @ViewBuilder errorContent: () -> ErrorContent, 15 | @ViewBuilder loadingContent: () -> LoadingContent = { DefaultLoadingView(loadingText: "Loading...") } 16 | ) -> some View { 17 | self.modifier(AKErrorViewModifier(pageState: pageState) { 18 | errorContent() 19 | } loadingContent: { 20 | loadingContent() 21 | }) 22 | } 23 | 24 | @ViewBuilder 25 | func akErrorView( 26 | pageState: Binding, 27 | action: @escaping () -> Void, 28 | @ViewBuilder loadingContent: () -> LoadingContent = { DefaultLoadingView(loadingText: "Loading...") } 29 | ) -> some View { 30 | self.modifier(AKErrorViewModifier(pageState: pageState) { 31 | DefaultErrorView(state: pageState) { 32 | action() 33 | } 34 | } loadingContent: { 35 | loadingContent() 36 | }) 37 | } 38 | 39 | @available(*, deprecated, renamed: "akErrorView", message: "") 40 | @ViewBuilder 41 | func errorableView(pageState: Binding, 42 | @ViewBuilder content: () -> Content, 43 | @ViewBuilder loadingContent: (() -> LoadingContent) = { DefaultLoadingView(loadingText: "Loading...") }) -> some View { 44 | self.modifier(AKErrorViewModifier(pageState: pageState) { 45 | content() 46 | } loadingContent: { 47 | loadingContent() 48 | }) 49 | } 50 | } 51 | 52 | public struct AKErrorViewModifier: ViewModifier { 53 | @State private var sheetTrigger: Bool = false 54 | @Binding var pageState: PageStates 55 | var errorContent: ErrorContent 56 | var loadingContent: LoadingContent 57 | 58 | public init(pageState: Binding, 59 | @ViewBuilder errorContent: () -> ErrorContent, 60 | @ViewBuilder loadingContent: () -> LoadingContent) { 61 | self._pageState = pageState 62 | self.errorContent = errorContent() 63 | self.loadingContent = loadingContent() 64 | } 65 | 66 | public func body(content: Content) -> some View { 67 | switch errorContent.type { 68 | case .onPage: 69 | onPageState(content: content) 70 | case .sheet: 71 | sheetState(content: content) 72 | case .fullScreen: 73 | sheetState(content: content) 74 | } 75 | } 76 | 77 | @ViewBuilder 78 | private func onPageState(content: Content) -> some View { 79 | switch pageState { 80 | case .failure: 81 | errorContent 82 | case .loading: 83 | switch loadingContent.type { 84 | case .onPage: 85 | loadingContent 86 | case .overlay: 87 | ZStack { 88 | content 89 | loadingContent 90 | } 91 | } 92 | DefaultLoadingView(loadingText: "Loading...") 93 | case .successful: 94 | content 95 | } 96 | } 97 | 98 | @ViewBuilder 99 | private func sheetState(content: Content) -> some View { 100 | Group { 101 | if pageState == .successful { 102 | content 103 | } else { 104 | switch loadingContent.type { 105 | case .onPage: 106 | loadingContent 107 | case .overlay: 108 | ZStack { 109 | content 110 | loadingContent 111 | } 112 | } 113 | } 114 | }.onChange(of: pageState) { newValue in 115 | sheetTrigger = (newValue == .failure) 116 | }.sheet(isPresented: $sheetTrigger) { 117 | errorContent 118 | } 119 | } 120 | 121 | #if os(iOS) 122 | @ViewBuilder 123 | private func fullscreenState(content: Content) -> some View { 124 | Group { 125 | if pageState == .successful { 126 | content 127 | } else { 128 | switch loadingContent.type { 129 | case .onPage: 130 | loadingContent 131 | case .overlay: 132 | ZStack { 133 | content 134 | loadingContent 135 | } 136 | } 137 | } 138 | }.onChange(of: pageState) { newValue in 139 | sheetTrigger = (newValue == .failure) 140 | }.fullScreenCover(isPresented: $sheetTrigger) { 141 | errorContent 142 | } 143 | } 144 | #endif 145 | } 146 | 147 | #if DEBUG 148 | @available(iOS 15.0, *) 149 | struct TestView: View { 150 | @State private var pageState: PageStates = .loading 151 | 152 | var body: some View { 153 | NavigationView { 154 | ScrollView { 155 | ForEach(0..<100, id: \.self) { _ in 156 | AsyncImage(url: URL(string: "https://picsum.photos/1000")) { phase in 157 | if let image = phase.image { 158 | image 159 | .resizable() 160 | .scaledToFill() 161 | } else { 162 | Color.gray 163 | } 164 | }.frame(height: 200, alignment: .center) 165 | .clipped() 166 | } 167 | }.navigationTitle("Example Content") 168 | } 169 | .akErrorView(pageState: $pageState) { 170 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) { 171 | pageState = .successful 172 | } 173 | } 174 | .onAppear { 175 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) { 176 | pageState = .failure 177 | } 178 | } 179 | } 180 | } 181 | 182 | #Preview { 183 | if #available(iOS 15.0, *) { 184 | TestView() 185 | } else { 186 | EmptyView() 187 | } 188 | } 189 | #endif 190 | -------------------------------------------------------------------------------- /Sources/ErrorableView/Enums/ErrorPresentTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorPresentTypes.swift 3 | // 4 | // 5 | // Created by Mehmet Ateş on 24.11.2023. 6 | // 7 | 8 | @frozen public enum ErrorPresentTypes { 9 | case onPage 10 | case fullScreen 11 | case sheet 12 | } 13 | 14 | @frozen public enum LoadingPresenterTypes { 15 | case onPage 16 | case overlay 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ErrorableView/Enums/PageStates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageStates.swift 3 | // 4 | // 5 | // Created by Mehmet Ateş on 24.11.2023. 6 | // 7 | 8 | @frozen public enum PageStates { 9 | case loading 10 | case successful 11 | case failure 12 | } 13 | -------------------------------------------------------------------------------- /Sources/ErrorableView/Views/DefaultErrorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultErrorView.swift 3 | // 4 | // 5 | // Created by Mehmet Ateş on 24.11.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public protocol ErrorableView: View { 11 | var type: ErrorPresentTypes { get set } 12 | } 13 | 14 | @frozen public struct DefaultErrorView: ErrorableView { 15 | @Environment(\.dismiss) private var dismiss 16 | @Binding private var state: PageStates 17 | private var uimodel: DefaultErrorPageUIModel 18 | private var buttonAction: (() -> Void)? 19 | public var type: ErrorPresentTypes 20 | 21 | public init( 22 | uimodel: DefaultErrorPageUIModel = .Builder().build(), 23 | type: ErrorPresentTypes = .sheet, 24 | state: Binding, 25 | buttonAction: (() -> Void)? = nil 26 | ) { 27 | self.uimodel = uimodel 28 | self.type = type 29 | self.buttonAction = buttonAction 30 | self._state = state 31 | } 32 | 33 | public var body: some View { 34 | VStack { 35 | closeButtonView 36 | Spacer() 37 | 38 | VStack { 39 | iconImageView 40 | .imageScale(.large) 41 | .font(.largeTitle) 42 | 43 | Text(uimodel.title) 44 | .font(.title) 45 | .fontWeight(.bold) 46 | .multilineTextAlignment(.center) 47 | } 48 | .padding(.bottom) 49 | 50 | subTitleView 51 | 52 | Spacer() 53 | 54 | buttonView 55 | }.padding(.vertical) 56 | } 57 | } 58 | 59 | // MARK: - UIComponents 60 | private extension DefaultErrorView { 61 | @ViewBuilder 62 | var closeButtonView: some View { 63 | if type != .onPage { 64 | HStack { 65 | Spacer() 66 | Button { 67 | buttonAction?() 68 | state = .loading 69 | dismiss() 70 | } label: { 71 | Image(systemName: "xmark.circle.fill") 72 | .font(.title) 73 | }.accentColor(.secondary) 74 | }.padding(.horizontal) 75 | } 76 | } 77 | 78 | @ViewBuilder 79 | var iconImageView: some View { 80 | if let icon = uimodel.icon { 81 | Image(icon) 82 | } else if let systemName = uimodel.systemName { 83 | Image(systemName: systemName) 84 | } 85 | } 86 | 87 | @ViewBuilder 88 | var subTitleView: some View { 89 | if let subtitle = uimodel.subtitle { 90 | Group { 91 | if #available(iOS 15.0, *) { 92 | Text(subtitle) 93 | .foregroundStyle(.secondary) 94 | } else { 95 | Text(subtitle) 96 | .foregroundColor(.secondary) 97 | } 98 | }.font(.headline) 99 | .multilineTextAlignment(.center) 100 | } 101 | } 102 | 103 | @ViewBuilder 104 | var buttonView: some View { 105 | if let buttonTitle = uimodel.buttonTitle { 106 | if #available(iOS 15.0, *) { 107 | Button { 108 | buttonAction?() 109 | state = .loading 110 | dismiss() 111 | } label: { 112 | Spacer() 113 | Text(buttonTitle) 114 | .bold() 115 | .padding(.vertical, 5) 116 | Spacer() 117 | }.buttonStyle(.borderedProminent) 118 | .padding(.horizontal) 119 | } else { 120 | Button { 121 | buttonAction?() 122 | } label: { 123 | Spacer() 124 | Text(buttonTitle) 125 | .bold() 126 | Spacer() 127 | }.modifier(ErrorStateButtonModifier()) 128 | .padding(.horizontal) 129 | } 130 | } 131 | } 132 | } 133 | 134 | // MARK: - UIModel 135 | @frozen public struct DefaultErrorPageUIModel { 136 | var title: LocalizedStringKey 137 | var subtitle: LocalizedStringKey? 138 | var icon: String? 139 | var systemName: String? 140 | var buttonTitle: LocalizedStringKey? 141 | 142 | public class Builder { 143 | private var type: ErrorPresentTypes = .sheet 144 | private var title: LocalizedStringKey = "Error!" 145 | private var subtitle: LocalizedStringKey? = "We encountered an error.\n Please try again later!" 146 | private var icon: String? 147 | private var systemName: String? = "externaldrive.fill.trianglebadge.exclamationmark" 148 | private var buttonTitle: LocalizedStringKey? = "Try Again!" 149 | 150 | public init() {} 151 | 152 | @discardableResult 153 | public func type(_ type: ErrorPresentTypes) -> Self { 154 | self.title = title 155 | return self 156 | } 157 | 158 | @discardableResult 159 | public func title(_ title: LocalizedStringKey) -> Self { 160 | self.title = title 161 | return self 162 | } 163 | 164 | @discardableResult 165 | public func subtitle(_ subtitle: LocalizedStringKey?) -> Self { 166 | self.subtitle = subtitle 167 | return self 168 | } 169 | 170 | @discardableResult 171 | public func icon(_ icon: String?) -> Self { 172 | self.icon = icon 173 | return self 174 | } 175 | 176 | @discardableResult 177 | public func systemName(_ systemName: String?) -> Self { 178 | self.systemName = systemName 179 | return self 180 | } 181 | 182 | @discardableResult 183 | public func buttonTitle(_ buttonTitle: LocalizedStringKey?) -> Self { 184 | self.buttonTitle = buttonTitle 185 | return self 186 | } 187 | 188 | public func build() -> DefaultErrorPageUIModel { 189 | DefaultErrorPageUIModel( 190 | title: title, 191 | subtitle: subtitle, 192 | icon: icon, 193 | systemName: systemName, 194 | buttonTitle: buttonTitle 195 | ) 196 | } 197 | } 198 | } 199 | 200 | // MARK: - ViewModifer(s) 201 | @frozen public struct ErrorStateButtonModifier: ViewModifier { 202 | public func body(content: Content) -> some View { 203 | content 204 | .padding(.vertical, 5) 205 | .foregroundColor(.primary) 206 | .background(Color.accentColor) 207 | .clipShape(RoundedRectangle(cornerRadius: 8)) 208 | } 209 | } 210 | 211 | #Preview { 212 | DefaultErrorView(state: .constant(.loading)) 213 | } 214 | -------------------------------------------------------------------------------- /Sources/ErrorableView/Views/DefaultLoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultLoadingView.swift 3 | // 4 | // 5 | // Created by Mehmet Ateş on 24.11.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public protocol LoadingView: View { 11 | var type: LoadingPresenterTypes { get set } 12 | } 13 | 14 | @frozen public struct DefaultLoadingView: LoadingView { 15 | private let loadingText: LocalizedStringKey 16 | private let progressViewColor: Color 17 | public var type: LoadingPresenterTypes 18 | 19 | public init( 20 | loadingText: LocalizedStringKey, 21 | progressViewColor: Color = .accentColor, 22 | type: LoadingPresenterTypes = .onPage 23 | ) { 24 | self.loadingText = loadingText 25 | self.progressViewColor = progressViewColor 26 | self.type = type 27 | } 28 | 29 | public var body: some View { 30 | #if os(macOS) 31 | ZStack { 32 | Rectangle() 33 | .opacity(type == .onPage ? 1 : 0.3) 34 | VStack { 35 | if #available(iOS 15.0, *) { 36 | ProgressView() 37 | .scaleEffect(1.2) 38 | .tint(progressViewColor) 39 | } else { 40 | ProgressView() 41 | .scaleEffect(1.2) 42 | } 43 | Text(loadingText) 44 | .font(.caption) 45 | .foregroundColor(.secondary) 46 | .padding(.top) 47 | } 48 | }.ignoresSafeArea() 49 | #else 50 | switch type { 51 | case .onPage: 52 | VStack { 53 | ProgressView() 54 | .scaleEffect(1.2) 55 | .tint(progressViewColor) 56 | 57 | Text(loadingText) 58 | .foregroundColor(.secondary) 59 | .padding(.top) 60 | } 61 | case .overlay: 62 | VStack { 63 | HStack { 64 | Spacer() 65 | } 66 | Spacer() 67 | ProgressView() 68 | .scaleEffect(1.2) 69 | .tint(progressViewColor) 70 | 71 | Text(loadingText) 72 | .foregroundColor(.secondary) 73 | .padding(.top) 74 | Spacer() 75 | }.background { 76 | Rectangle() 77 | .foregroundStyle(.ultraThinMaterial) 78 | }.ignoresSafeArea() 79 | } 80 | #endif 81 | } 82 | } 83 | 84 | #Preview { 85 | DefaultLoadingView(loadingText: "Loading...") 86 | } 87 | -------------------------------------------------------------------------------- /Tests/ErrorableViewTests/ErrorableViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ErrorableView 3 | 4 | final class ErrorableViewTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | --------------------------------------------------------------------------------