Skip to content

Commit 254bb89

Browse files
committed
Add the basics of ViewStateController and withViewStateModifier
1 parent ab0e6dc commit 254bb89

20 files changed

+1182
-0
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## User settings
2+
xcuserdata/
3+
4+
# Swift Package Manager
5+
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
6+
# hence it is not needed unless you have added a package configuration file to your project
7+
.swiftpm
8+
9+
.build/

BuildTools/.swiftformat

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# format options
2+
--allman false
3+
--indent 4
4+
--maxwidth 120
5+
--commas inline
6+
--decimalgrouping 3
7+
--wraparguments before-first
8+
9+
# rules
10+
--enable isEmpty
11+
--disable redundantType
12+
--disable wrapMultilineStatementBraces

Package.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// swift-tools-version: 5.7
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: "ViewStateController", platforms: [
8+
.iOS(.v15),
9+
.macOS(.v12)
10+
],
11+
products: [
12+
.library(
13+
name: "ViewStateController",
14+
targets: ["ViewStateController"]
15+
)
16+
],
17+
dependencies: [],
18+
targets: [
19+
.target(
20+
name: "ViewStateController",
21+
dependencies: []
22+
),
23+
.testTarget(
24+
name: "ViewStateControllerTests",
25+
dependencies: ["ViewStateController"]
26+
)
27+
]
28+
)

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
11
# ViewStateController
22

33
ViewStateController is a framework for Swift and SwiftUI developers that provides a simple and flexible way to manage the state of views that load information from a backend. It allows you to handle different states based on a historical array of states, and provides properties and methods to help you access and modify the state. With ViewStateController, you can easily implement complex views that depend on asynchronous data loading, and create a better user experience by showing loading spinners or error messages.
4+
5+
## ViewStateController Object
6+
7+
## WithViewState Modifier
8+
9+
## Examples with code samples
10+
11+
12+
## BuildTools
13+
14+
To run the formatter, just run:
15+
16+
`.scripts/run-formatter.sh` from the root of the repository.
17+
18+
YYou need to have `SwiftFormat` installed (`brew install swiftformat`).
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
//
2+
// ErrorView.swift
3+
// ViewStateController
4+
//
5+
// Created by Manu on 26/02/2023.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
11+
/// Customizable error view with different types.
12+
public struct ErrorView: View {
13+
private let type: ErrorViewType
14+
15+
/// Initializer
16+
/// - Parameters:
17+
/// - type: The type to apply the style of the error state.
18+
public init(type: ErrorViewType) {
19+
self.type = type
20+
}
21+
22+
/// Convenience initializer that applies the default error state.
23+
/// - Parameter retryAction: The retry action for the button.
24+
public init(retryAction: @escaping () -> Void) {
25+
type = .vertical(
26+
style: .init(
27+
buttonOptions: .init(action: retryAction),
28+
frameAlignment: .center
29+
),
30+
alignment: .center
31+
)
32+
}
33+
34+
public var body: some View {
35+
switch type {
36+
case .emptyView:
37+
EmptyView()
38+
case let .vertical(style, alignment):
39+
verticalView(style: style, alignment: alignment)
40+
case let .horizontal(style, alignment):
41+
horizontalView(style: style, alignment: alignment)
42+
case let .custom(customView):
43+
customView
44+
}
45+
}
46+
}
47+
48+
private extension ErrorView {
49+
@ViewBuilder
50+
func verticalView(
51+
style: ErrorViewStyle,
52+
alignment: HorizontalAlignment
53+
) -> some View {
54+
VStack(alignment: alignment) {
55+
view(for: style)
56+
}
57+
.frame(maxWidth: .infinity, alignment: style.frameAlignment)
58+
}
59+
60+
@ViewBuilder
61+
func horizontalView(
62+
style: ErrorViewStyle,
63+
alignment: VerticalAlignment
64+
) -> some View {
65+
HStack(alignment: alignment) {
66+
view(for: style)
67+
}
68+
.frame(maxWidth: .infinity, alignment: style.frameAlignment)
69+
}
70+
71+
@ViewBuilder
72+
func view(for style: ErrorViewStyle) -> some View {
73+
if let imageOptions = style.imageOptions {
74+
Image(systemName: imageOptions.systemName)
75+
.resizable()
76+
.frame(
77+
width: imageOptions.frame.width,
78+
height: imageOptions.frame.height
79+
)
80+
.foregroundColor(imageOptions.foregroundColor)
81+
}
82+
if let titleOptions = style.titleOptions {
83+
Text(titleOptions.text)
84+
.font(titleOptions.font)
85+
.foregroundColor(titleOptions.foregroundColor)
86+
}
87+
if let buttonOptions = style.buttonOptions {
88+
Button(buttonOptions.text) {
89+
buttonOptions.action()
90+
}
91+
.tint(buttonOptions.tintColor)
92+
.foregroundColor(buttonOptions.foregroundColor)
93+
.buttonStyle(.borderedProminent)
94+
.font(buttonOptions.font)
95+
}
96+
}
97+
}
98+
99+
/// Possible types of error view.
100+
public enum ErrorViewType {
101+
/// Displays an EmptyView.
102+
case emptyView
103+
/// Displays a vertical stack with some customizable options:
104+
/// Image (system name, color, frame)
105+
/// Title (text, color, font)
106+
/// Retry button (text, foreground/background color, font, action)
107+
case vertical(
108+
style: ErrorViewStyle,
109+
alignment: HorizontalAlignment
110+
)
111+
/// Displays an horizontal stack with some customizable options:
112+
/// Image (system name, color, frame)
113+
/// Title (text, color, font)
114+
/// Retry button (text, foreground/background color, font, action)
115+
case horizontal(
116+
style: ErrorViewStyle,
117+
alignment: VerticalAlignment
118+
)
119+
/// Displays a custom view.
120+
case custom(_ view: AnyView)
121+
}
122+
123+
public struct ErrorViewStyle {
124+
public struct ImageOptions {
125+
let systemName: String
126+
let foregroundColor: Color
127+
let frame: CGSize
128+
129+
public init(
130+
systemName: String = "xmark.circle.fill",
131+
foregroundColor: Color = .red,
132+
frame: CGSize = .init(width: 50, height: 50)
133+
) {
134+
self.systemName = systemName
135+
self.foregroundColor = foregroundColor
136+
self.frame = frame
137+
}
138+
}
139+
140+
public struct TitleOptions {
141+
let text: String
142+
let foregroundColor: Color
143+
let font: Font
144+
145+
public init(
146+
text: String = "Something went wrong",
147+
foregroundColor: Color = .primary,
148+
font: Font = .callout
149+
) {
150+
self.text = text
151+
self.foregroundColor = foregroundColor
152+
self.font = font
153+
}
154+
}
155+
156+
public struct ButtonOptions {
157+
let text: String
158+
let foregroundColor: Color
159+
let tintColor: Color
160+
let font: Font
161+
let action: () -> Void
162+
163+
public init(
164+
text: String = "Retry",
165+
foregroundColor: Color = .white,
166+
tintColor: Color = .accentColor,
167+
font: Font = .callout,
168+
action: @escaping () -> Void
169+
) {
170+
self.text = text
171+
self.foregroundColor = foregroundColor
172+
self.tintColor = tintColor
173+
self.font = font
174+
self.action = action
175+
}
176+
}
177+
178+
let imageOptions: ImageOptions?
179+
let titleOptions: TitleOptions?
180+
let buttonOptions: ButtonOptions?
181+
let frameAlignment: Alignment
182+
183+
public init(
184+
imageOptions: ImageOptions? = .init(),
185+
titleOptions: TitleOptions? = .init(),
186+
buttonOptions: ButtonOptions?,
187+
frameAlignment: Alignment = .center
188+
) {
189+
self.imageOptions = imageOptions
190+
self.titleOptions = titleOptions
191+
self.buttonOptions = buttonOptions
192+
self.frameAlignment = frameAlignment
193+
}
194+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// AnyViewWrapper.swift
3+
// ViewStateController
4+
//
5+
// Created by Manu on 23/02/2023.
6+
//
7+
8+
import SwiftUI
9+
10+
public extension View {
11+
/// Embeds the current view inside an AnyView.
12+
func asAnyView() -> AnyView {
13+
AnyView(self)
14+
}
15+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// IfModifier.swift
3+
// ViewStateController
4+
//
5+
// Created by Manu on 27/02/2023.
6+
//
7+
8+
import SwiftUI
9+
10+
extension View {
11+
/// Applies the given transform if the given condition evaluates to `true`.
12+
/// - Parameters:
13+
/// - condition: The condition to evaluate.
14+
/// - transform: The transform to apply to the source `View`.
15+
/// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
16+
@ViewBuilder
17+
func `if`(_ condition: Bool, transform: (Self) -> some View) -> some View {
18+
if condition {
19+
transform(self)
20+
} else {
21+
self
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)