Skip to content

Commit ae784e6

Browse files
committed
Initial commit
0 parents  commit ae784e6

File tree

11 files changed

+372
-0
lines changed

11 files changed

+372
-0
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

.gitignore

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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/

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.

Package.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// swift-tools-version: 5.9
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+
products: [
9+
// Products define the executables and libraries a package produces, making them visible to other packages.
10+
.library(
11+
name: "ErrorableView",
12+
targets: ["ErrorableView"]),
13+
],
14+
targets: [
15+
// Targets are the basic building blocks of a package, defining a module or a test suite.
16+
// Targets can depend on other targets in this package and products from dependencies.
17+
.target(
18+
name: "ErrorableView"),
19+
.testTarget(
20+
name: "ErrorableViewTests",
21+
dependencies: ["ErrorableView"]),
22+
]
23+
)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# ErrorableView-SwiftUI
2+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// ErrorableViewModelProtocol.swift
3+
//
4+
//
5+
// Created by Mehmet Ateş on 26.08.2023.
6+
//
7+
8+
import Combine
9+
10+
@available(macOS 10.15, *)
11+
@available(iOS 13.0, *)
12+
public protocol ErrorableViewModelProtocol: AnyObject, ObservableObject {
13+
var state: PageStates { get set }
14+
func refresh()
15+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
//
2+
// ErrorableViewModelProtocol.swift
3+
//
4+
//
5+
// Created by Mehmet Ateş on 26.08.2023.
6+
//
7+
8+
import SwiftUI
9+
10+
@available(macOS 11.0, *)
11+
@available(iOS 14.0, *)
12+
public protocol ErrorableViewProtocol: View where ViewModel: ErrorableViewModelProtocol {
13+
associatedtype ViewModel
14+
associatedtype Content
15+
16+
var viewModel: ViewModel { get set }
17+
}
18+
19+
@available(macOS 11.0, *)
20+
@available(iOS 14.0, *)
21+
public extension ErrorableViewProtocol where Content == AnyView {
22+
func createErrorableView(
23+
@ViewBuilder loadingView: () -> Content,
24+
@ViewBuilder failureView: () -> Content,
25+
@ViewBuilder successfulView: () -> Content
26+
) -> Content {
27+
switch viewModel.state {
28+
case .loading:
29+
loadingView()
30+
case .successful:
31+
successfulView()
32+
case .failure:
33+
failureView()
34+
}
35+
}
36+
37+
func createErrorableView(
38+
@ViewBuilder failureView: () -> Content,
39+
@ViewBuilder successfulView: () -> Content
40+
) -> Content {
41+
switch viewModel.state {
42+
case .loading:
43+
loadingState
44+
case .successful:
45+
successfulView()
46+
case .failure:
47+
failureView()
48+
}
49+
}
50+
51+
func createErrorableView(
52+
errorTitle: LocalizedStringKey,
53+
errorSubTitle: LocalizedStringKey? = nil,
54+
errorIcon: String? = nil,
55+
errorSystemIcon: String? = nil,
56+
errorButtonTitle: LocalizedStringKey,
57+
@ViewBuilder successfulView: () -> Content
58+
) -> Content {
59+
switch viewModel.state {
60+
case .loading:
61+
loadingState
62+
case .successful:
63+
successfulView()
64+
case .failure:
65+
failuteState(errorTitle: errorTitle, errorSubTitle: errorSubTitle, errorIcon: errorIcon, errorSystemIcon: errorSystemIcon, errorButtonTitle: errorButtonTitle)
66+
}
67+
}
68+
69+
private var loadingState: Content {
70+
AnyView(
71+
VStack {
72+
Spacer()
73+
ProgressView()
74+
.accentColor(.accentColor)
75+
Spacer()
76+
}
77+
)
78+
}
79+
80+
private func failuteState(
81+
errorTitle: LocalizedStringKey,
82+
errorSubTitle: LocalizedStringKey?,
83+
errorIcon: String?,
84+
errorSystemIcon: String?,
85+
errorButtonTitle: LocalizedStringKey
86+
) -> Content {
87+
AnyView(
88+
VStack {
89+
Spacer()
90+
if let errorSystemIcon {
91+
Image(systemName: errorSystemIcon)
92+
.font(.system(size: 60))
93+
.foregroundColor(.red)
94+
}
95+
96+
if let errorIcon {
97+
Image(errorIcon)
98+
}
99+
100+
Text(errorTitle)
101+
.font(.title)
102+
.fontWeight(.bold)
103+
.multilineTextAlignment(.center)
104+
.padding(.top)
105+
if let errorSubTitle {
106+
Text(errorSubTitle)
107+
.font(.headline)
108+
.fontWeight(.bold)
109+
.foregroundColor(.secondary)
110+
.multilineTextAlignment(.center)
111+
}
112+
113+
Spacer()
114+
Button {
115+
viewModel.refresh()
116+
} label: {
117+
Text(errorButtonTitle)
118+
.padding()
119+
.background(Color.red.opacity(0.3))
120+
}.accentColor(Color.red)
121+
.clipShape(Capsule())
122+
Spacer()
123+
}
124+
)
125+
}
126+
}
127+
128+
@available(macOS 11.0, *)
129+
@available(iOS 15.0, *)
130+
private struct Example_Preview: PreviewProvider {
131+
static var previews: some View {
132+
Example()
133+
}
134+
}
135+
136+
@available(macOS 11.0, *)
137+
@available(iOS 15.0, *)
138+
private struct Example: ErrorableViewProtocol {
139+
typealias Content = AnyView
140+
typealias ViewModel = ExampleViewModel
141+
@ObservedObject var viewModel: ExampleViewModel = ExampleViewModel()
142+
143+
var body: some View {
144+
NavigationView {
145+
createErrorableView(errorTitle: "Upps!", errorSubTitle: "We encountered an error.\n Please try again later!", errorSystemIcon: "minus.diamond.fill", errorButtonTitle: "Try Again") {
146+
AnyView (
147+
ScrollView {
148+
ForEach(0..<100, id: \.self) { _ in
149+
AsyncImage(url: URL(string: "https://picsum.photos/200")) { phase in
150+
if let image = phase.image {
151+
image
152+
.resizable()
153+
.scaledToFill()
154+
} else {
155+
Color.gray
156+
}
157+
}.frame(width: 300, height: 200, alignment: .center)
158+
}
159+
}
160+
)
161+
}
162+
.navigationTitle("Example")
163+
}.onAppear {
164+
Task { @MainActor in
165+
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
166+
viewModel.state = .failure
167+
}
168+
}
169+
}
170+
}
171+
}
172+
173+
@available(macOS 11.0, *)
174+
@available(iOS 15.0, *)
175+
private final class ExampleViewModel: ErrorableViewModelProtocol {
176+
@Published var state: PageStates = .loading
177+
func refresh() {
178+
state = .successful
179+
}
180+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// PageStates.swift
3+
//
4+
//
5+
// Created by Mehmet Ateş on 26.08.2023.
6+
//
7+
8+
public enum PageStates {
9+
case loading
10+
case successful
11+
case failure
12+
}

0 commit comments

Comments
 (0)