├── .github
├── FUNDING.yml
└── workflows
│ └── swift.yml
├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── ObjectUI
│ ├── Data
│ └── Object.swift
│ └── View
│ ├── ObjectView.swift
│ └── StateObjectView.swift
└── Tests
└── ObjectUITests
└── ObjectUITests.swift
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [0xLeif] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-Fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | name: Swift
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: macOS-latest
9 | strategy:
10 | matrix:
11 | destination: ['platform=iOS Simulator,OS=13.1,name=iPhone 11']
12 | xcode: ['/Applications/Xcode_12.app/Contents/Developer']
13 | steps:
14 | - uses: actions/checkout@v1
15 | # Github Actions' machines do in fact have recent versions of Xcode,
16 | # but you may have to explicitly switch to them. We explicitly want
17 | # to use Xcode 12, so we use xcode-select to switch to it.
18 | - name: Switch to Xcode 12
19 | run: sudo xcode-select --switch /Applications/Xcode_12.app
20 | # Since we want to be running our tests from Xcode, we need to
21 | # generate an .xcodeproj file. Luckly, Swift Package Manager has
22 | # build in functionality to do so.
23 | - name: Generate xcodeproj
24 | run: swift package generate-xcodeproj
25 | # Finally, we invoke xcodebuild to run the tests on an iPhone 11
26 | # simulator.
27 | - name: Run tests
28 | run: xcodebuild test -destination 'name=iPhone 11' -scheme 'ObjectUI-Package'
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | /Package.resolved
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Zach Eriksen
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.3
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: "ObjectUI",
8 | platforms: [
9 | .iOS(.v14),
10 | .macOS(.v11),
11 | .watchOS(.v7),
12 | .tvOS(.v14)
13 | ],
14 | products: [
15 | // Products define the executables and libraries a package produces, and make them visible to other packages.
16 | .library(
17 | name: "ObjectUI",
18 | targets: ["ObjectUI"]),
19 | ],
20 | dependencies: [
21 | // Dependencies declare other packages that this package depends on.
22 | // .package(url: /* package url */, from: "1.0.0"),
23 | .package(url: "https://github.com/0xLet/WTV", from: "0.1.1"),
24 | .package(url: "https://github.com/0xLet/SURL", from: "0.1.0"),
25 | .package(url: "https://github.com/0xLet/SwiftFu", from: "1.0.1"),
26 | .package(url: "https://github.com/0xLeif/Chronicle", from: "0.2.3"),
27 | .package(url: "https://github.com/0xLeif/Task", from: "1.0.0"),
28 | .package(url: "https://github.com/0xLeif/Yarn", from: "1.0.0")
29 | ],
30 | targets: [
31 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
32 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
33 | .target(
34 | name: "ObjectUI",
35 | dependencies: [
36 | "WTV",
37 | "SURL",
38 | "SwiftFu",
39 | "Chronicle",
40 | "Task",
41 | "Yarn"
42 | ]),
43 | .testTarget(
44 | name: "ObjectUITests",
45 | dependencies: ["ObjectUI"]),
46 | ]
47 | )
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## [Prefer CacheStore](https://github.com/0xOpenBytes/CacheStore)
2 |
3 | # ObjectUI
4 |
5 | *Create SwiftUI Views with any data*
6 |
7 | ## Usage
8 | ```swift
9 | import ObjectUI
10 | ```
11 |
12 | ### Basic Examples
13 |
14 | #### ObjectView
15 | ```swift
16 | struct ContentView: View {
17 | var body: some View {
18 | ObjectView(data: "Hello, World 👋") { object in
19 | if let text = object.value(as: String.self) {
20 | Text(text)
21 | } else {
22 | ProgressView()
23 | }
24 | }
25 | }
26 | }
27 | ```
28 |
29 | #### StateObjectView
30 | ```swift
31 | struct ContentView: View {
32 | var body: some View {
33 | StateObjectView { object in
34 | if let text = object.value(as: String.self) {
35 | Text(text)
36 | } else {
37 | ProgressView()
38 | .onAppear {
39 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
40 | object.set(value: "👋")
41 | }
42 | }
43 | }
44 | }
45 | }
46 | }
47 | ```
48 |
49 | ### JSON Example
50 | ```swift
51 | struct ContentView: View {
52 | let json = """
53 | {
54 | "userId": 1,
55 | "id": 2,
56 | "title": "quis ut nam facilis et officia qui",
57 | "completed": false
58 | }
59 | """
60 | .data(using: .utf8)
61 |
62 | var body: some View {
63 | ObjectView(data: json) { object in
64 | VStack {
65 | object.userId.value(as: Int.self).map { userId in
66 | Text("\(userId)")
67 | }
68 |
69 | object.id.value(as: Int.self).map { id in
70 | Text("\(id)")
71 | }
72 |
73 | object.title.value(as: String.self).map { title in
74 | Text(title)
75 | }
76 |
77 | object.completed.value(as: Bool.self).map { completed in
78 | Text(completed.description)
79 | }
80 | }
81 | }
82 | }
83 | }
84 | ```
85 |
--------------------------------------------------------------------------------
/Sources/ObjectUI/Data/Object.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Object.swift
3 | // ObjectUI
4 | //
5 | // Created by Leif on 5/24/21.
6 | //
7 |
8 | import SwiftFu
9 | import SwiftUI
10 |
11 | @dynamicMemberLookup
12 | public class Object: FuableClass, ObservableObject {
13 | public enum ObjectVariable: String, Hashable {
14 | case value
15 | case child
16 | case array
17 | case json
18 | }
19 |
20 | /// Variables of the object
21 | @Published public var variables: [AnyHashable: Any] = [:]
22 |
23 | /// @dynamicMemberLookup
24 | public subscript(dynamicMember member: String) -> Object {
25 | variable(named: member)
26 | }
27 |
28 | // MARK: public init
29 |
30 | public init() { }
31 | public convenience init(_ closure: (Object) -> Void) {
32 | self.init()
33 |
34 | closure(self)
35 | }
36 | public init(_ value: Any? = nil, _ closure: ((Object) -> Void)? = nil) {
37 | defer {
38 | if let closure = closure {
39 | configure(closure)
40 | }
41 | }
42 |
43 | guard let value = value else {
44 | return
45 | }
46 | let unwrappedValue = unwrap(value)
47 | if let _ = unwrappedValue as? NSNull {
48 | return
49 | }
50 | if let object = unwrappedValue as? Object {
51 | consume(object)
52 | } else if let array = unwrappedValue as? [Any] {
53 | consume(Object(array: array))
54 | } else if let dictionary = unwrappedValue as? [AnyHashable: Any] {
55 | consume(Object(dictionary: dictionary))
56 | } else if let data = unwrappedValue as? Data {
57 | consume(Object(data: data))
58 | } else {
59 | consume(Object().set(value: unwrappedValue))
60 | }
61 | }
62 |
63 | // MARK: private init
64 |
65 | private init(array: [Any]) {
66 | set(
67 | variable: ObjectVariable.array.rawValue,
68 | value: array.map { Object($0) }
69 | )
70 | }
71 | private init(dictionary: [AnyHashable: Any]) {
72 | variables = dictionary
73 | }
74 | private init(data: Data) {
75 | defer {
76 | set(variable: ObjectVariable.json.rawValue, value: String(data: data, encoding: .utf8))
77 | set(value: data)
78 | }
79 | if let json = try? JSONSerialization.jsonObject(with: data,
80 | options: .allowFragments) as? [Any] {
81 | set(variable: ObjectVariable.array.rawValue, value: json)
82 | return
83 | }
84 | guard let json = try? JSONSerialization.jsonObject(with: data,
85 | options: .allowFragments) as? [AnyHashable: Any] else {
86 | return
87 | }
88 | consume(Object(json))
89 | }
90 | }
91 |
92 | // MARK: public variables
93 |
94 |
95 | public extension Object {
96 | var array: [Object] {
97 | if let array = variables[ObjectVariable.array.rawValue] as? [Data] {
98 | return array.map { Object(data: $0) }
99 | } else if let array = variables[ObjectVariable.array.rawValue] as? [Any] {
100 | return array.map { value in
101 | guard let json = value as? [AnyHashable: Any] else {
102 | return Object(value)
103 | }
104 | return Object(dictionary: json)
105 | }
106 | }
107 | return []
108 | }
109 |
110 | var child: Object {
111 | (variables[ObjectVariable.child.rawValue] as? Object) ?? Object()
112 | }
113 |
114 | var json: Object {
115 | (variables[ObjectVariable.json.rawValue] as? Object) ?? Object()
116 | }
117 |
118 | var value: Any {
119 | variables[ObjectVariable.value.rawValue] ?? Object()
120 | }
121 | }
122 |
123 | // MARK: public functions
124 |
125 |
126 | public extension Object {
127 | /// Retrieve a Value from the current object
128 | @discardableResult
129 | func variable(named: AnyHashable) -> Object {
130 | guard let value = variables[named] else {
131 | return Object()
132 | }
133 | if let array = value as? [Any] {
134 | return Object(array: array)
135 | }
136 | guard let object = value as? Object else {
137 | return Object(unwrap(value))
138 | }
139 | return object
140 | }
141 | /// Set a named Value to the current object
142 | @discardableResult
143 | func set(variable named: AnyHashable = ObjectVariable.value.rawValue, value: Any?) -> Self {
144 | guard let value = value,
145 | (unwrap(value) as? NSNull) == nil else {
146 | return self
147 | }
148 |
149 | variables[named] = value
150 |
151 | return self
152 | }
153 | /// Modify a Value with a name to the current object
154 | @discardableResult
155 | func modify(variable named: AnyHashable = ObjectVariable.value.rawValue, modifier: (T?) -> T?) -> Self {
156 | guard let variable = variables[named],
157 | let value = variable as? T else {
158 | variables[named] = modifier(nil)
159 |
160 | return self
161 | }
162 | variables[named] = modifier(value)
163 |
164 | return self
165 | }
166 | /// Update a Value with a name to the current object
167 | @discardableResult
168 | func update(variable named: AnyHashable = ObjectVariable.value.rawValue, modifier: (T) -> T) -> Self {
169 | guard let variable = variables[named],
170 | let value = variable as? T else {
171 | return self
172 | }
173 | variables[named] = modifier(value)
174 |
175 | return self
176 | }
177 | /// Set the ChildObject with a name of `_object` to the current object
178 | @discardableResult
179 | func set(childObject object: Object) -> Self {
180 | variables[ObjectVariable.child.rawValue] = object
181 |
182 | return self
183 | }
184 | /// Set the Array with a name of `_array` to the current object
185 | @discardableResult
186 | func set(array: [Any]) -> Self {
187 | variables[ObjectVariable.array.rawValue] = array
188 |
189 | return self
190 | }
191 |
192 | @discardableResult
193 | func configure(_ closure: (Object) -> Void) -> Object {
194 | closure(self)
195 |
196 | return self
197 | }
198 |
199 | @discardableResult
200 | func consume(_ object: Object) -> Object {
201 | object.variables.forEach { (key, value) in
202 | self.set(variable: key, value: value)
203 | }
204 |
205 | return self
206 | }
207 |
208 | func value(as type: T.Type? = nil) -> T? {
209 | value as? T
210 | }
211 |
212 | func value(decodedAs type: T.Type) -> T? where T: Decodable {
213 | guard let data = value(as: Data.self) else {
214 | return nil
215 | }
216 |
217 | return try? JSONDecoder().decode(T.self, from: data)
218 | }
219 | }
220 |
221 | private extension Object {
222 | /// Unwraps the Any type
223 | func unwrap(_ value: Any) -> Any {
224 | let mValue = Mirror(reflecting: value)
225 | let isValueOptional = mValue.displayStyle != .optional
226 | let isValueEmpty = mValue.children.isEmpty
227 | if isValueOptional { return value }
228 | if isValueEmpty { return NSNull() }
229 | guard let (_, unwrappedValue) = mValue.children.first else { return NSNull() }
230 | return unwrappedValue
231 | }
232 | }
233 |
234 | extension Object: CustomStringConvertible {
235 | public var description: String {
236 | """
237 | Object {
238 | \(
239 | variables
240 | .map { (key, value) in
241 | guard let object = value as? Object else {
242 | return "|\t* \(key): \(value) (\(type(of: value)))"
243 | }
244 |
245 | let values = object.description.split(separator: "\n")
246 | .dropFirst()
247 |
248 | if values.dropLast().isEmpty {
249 | return "|\t* \(key): Object { }"
250 | }
251 |
252 | return "|\t* \(key): Object {\n\(values.map { "|\t \($0)" }.joined(separator: "\n"))"
253 | }
254 | .joined(separator: "\n")
255 | )
256 | }
257 | """
258 | }
259 | }
260 |
261 | extension Object: Hashable {
262 | public static func == (lhs: Object, rhs: Object) -> Bool {
263 | lhs.description == rhs.description
264 | }
265 |
266 | public func hash(into hasher: inout Hasher) {
267 | hasher.combine(description)
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/Sources/ObjectUI/View/ObjectView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObjectView.swift
3 | // ObjectUI
4 | //
5 | // Created by Leif on 5/24/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct ObjectView: View where Content: View {
11 | @ObservedObject private var object: Object
12 |
13 | private let content: (Object) -> Content
14 |
15 | public init(
16 | data: Any? = nil,
17 | @ViewBuilder content: @escaping (Object) -> Content
18 | ) {
19 | self.object = Object(data)
20 | self.content = content
21 | }
22 |
23 | public var body: some View {
24 | content(object)
25 | }
26 | }
27 |
28 | struct ObjectView_Previews: PreviewProvider {
29 | static var previews: some View {
30 | ObjectView(data: "Hello, World 👋") { object in
31 | if let text = object.value(as: String.self) {
32 | Text(text)
33 | } else {
34 | ProgressView()
35 |
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/ObjectUI/View/StateObjectView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObjectView.swift
3 | // ObjectUI
4 | //
5 | // Created by Leif on 5/24/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct StateObjectView: View where Content: View {
11 | @StateObject private var object: Object = Object()
12 |
13 | private let content: (Object) -> Content
14 |
15 | public init(
16 | @ViewBuilder content: @escaping (Object) -> Content
17 | ) {
18 | self.content = content
19 | }
20 |
21 | public var body: some View {
22 | content(object)
23 | }
24 | }
25 |
26 | struct StateObjectView_Previews: PreviewProvider {
27 | static var previews: some View {
28 | StateObjectView { object in
29 | if let text = object.value(as: String.self) {
30 | Text(text)
31 | } else {
32 | ProgressView()
33 | .onAppear {
34 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
35 | object.set(value: "👋")
36 | }
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/ObjectUITests/ObjectUITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import ObjectUI
3 |
4 | final class ObjectUITests: XCTestCase {
5 | func testExample() {
6 | enum Value: String {
7 | case text
8 | }
9 | let object = Object()
10 | object.set(variable: Value.text, value: "Hello, World!")
11 |
12 | XCTAssertEqual(object.variable(named: Value.text).value(), "Hello, World!")
13 | }
14 |
15 | func testTextVariable() {
16 | let object = Object()
17 | object.set(variable: "text", value: "Hello, World!")
18 |
19 | XCTAssertEqual(object.text.value(), "Hello, World!")
20 | }
21 |
22 | func testArray() {
23 | let json = """
24 | [
25 | {
26 | "userId": 19,
27 | "id": 2,
28 | "title": "quis ut nam facilis et officia qui",
29 | "completed": false
30 | },
31 | {
32 | "userId": 1,
33 | "id": 8,
34 | "title": "quo adipisci enim quam ut ab",
35 | "completed": true
36 | }
37 | ]
38 | """
39 | .data(using: .utf8)
40 |
41 | let object = Object(json)
42 |
43 | XCTAssertEqual(object.array.count, 2)
44 |
45 | let post = object.array[0]
46 |
47 | XCTAssertEqual(post.userId.value(), 19)
48 | XCTAssertEqual(post.id.value(), 2)
49 | XCTAssertEqual(post.title.value(), "quis ut nam facilis et officia qui")
50 | XCTAssertEqual(post.completed.value(), false)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------