├── LICENSE
├── NetworkingLayerSwift6.drawio
├── NetworkingLayerSwift6.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcuserdata
│ │ └── egzonpllana.xcuserdatad
│ │ └── IDEFindNavigatorScopes.plist
└── xcuserdata
│ └── egzonpllana.xcuserdatad
│ ├── xcdebugger
│ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ └── xcschememanagement.plist
├── NetworkingLayerSwift6
├── Architecture
│ ├── Core
│ │ └── API
│ │ │ ├── APIEndpoints.swift
│ │ │ ├── APIVersion.swift
│ │ │ └── Interceptors.swift
│ ├── Data
│ │ ├── Entities
│ │ │ └── PostDTO.swift
│ │ └── PostsRepository.swift
│ ├── Domain
│ │ ├── Protocols
│ │ │ ├── PostsRepositoryProtocol.swift
│ │ │ └── PostsRepositoryUseCaseProtocol.swift
│ │ └── UseCases
│ │ │ └── PostsRepositoryUseCase.swift
│ ├── NetworkingLayerSwift6App.swift
│ └── Presentation
│ │ └── Home
│ │ ├── HomeView.swift
│ │ ├── HomeViewModel.swift
│ │ └── HomeViewModelProtocol.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
└── Resources
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ └── image-png.png
└── README.md
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Egzon Pllana
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 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 77;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 5206A5412D8F714C00E393D8 /* EventHorizon in Frameworks */ = {isa = PBXBuildFile; productRef = 5206A5402D8F714C00E393D8 /* EventHorizon */; };
11 | 522AB0DA2D88E72000D733F0 /* EventHorizon in Frameworks */ = {isa = PBXBuildFile; productRef = 522AB0D92D88E72000D733F0 /* EventHorizon */; };
12 | /* End PBXBuildFile section */
13 |
14 | /* Begin PBXContainerItemProxy section */
15 | 5240D4D42C72A90E000F58CD /* PBXContainerItemProxy */ = {
16 | isa = PBXContainerItemProxy;
17 | containerPortal = 52EB7E402C70B90400069B79 /* Project object */;
18 | proxyType = 1;
19 | remoteGlobalIDString = 52EB7E472C70B90400069B79;
20 | remoteInfo = NetworkingLayerSwift6;
21 | };
22 | /* End PBXContainerItemProxy section */
23 |
24 | /* Begin PBXFileReference section */
25 | 5240D4D02C72A90E000F58CD /* NetworkingLayerSwift6Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NetworkingLayerSwift6Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
26 | 52EB7E482C70B90400069B79 /* NetworkingLayerSwift6.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetworkingLayerSwift6.app; sourceTree = BUILT_PRODUCTS_DIR; };
27 | /* End PBXFileReference section */
28 |
29 | /* Begin PBXFileSystemSynchronizedRootGroup section */
30 | 52EB7E4A2C70B90400069B79 /* NetworkingLayerSwift6 */ = {
31 | isa = PBXFileSystemSynchronizedRootGroup;
32 | path = NetworkingLayerSwift6;
33 | sourceTree = "";
34 | };
35 | /* End PBXFileSystemSynchronizedRootGroup section */
36 |
37 | /* Begin PBXFrameworksBuildPhase section */
38 | 5240D4CD2C72A90E000F58CD /* Frameworks */ = {
39 | isa = PBXFrameworksBuildPhase;
40 | buildActionMask = 2147483647;
41 | files = (
42 | );
43 | runOnlyForDeploymentPostprocessing = 0;
44 | };
45 | 52EB7E452C70B90400069B79 /* Frameworks */ = {
46 | isa = PBXFrameworksBuildPhase;
47 | buildActionMask = 2147483647;
48 | files = (
49 | 522AB0DA2D88E72000D733F0 /* EventHorizon in Frameworks */,
50 | 5206A5412D8F714C00E393D8 /* EventHorizon in Frameworks */,
51 | );
52 | runOnlyForDeploymentPostprocessing = 0;
53 | };
54 | /* End PBXFrameworksBuildPhase section */
55 |
56 | /* Begin PBXGroup section */
57 | 52EB7E3F2C70B90400069B79 = {
58 | isa = PBXGroup;
59 | children = (
60 | 52EB7E4A2C70B90400069B79 /* NetworkingLayerSwift6 */,
61 | 52EB7E492C70B90400069B79 /* Products */,
62 | );
63 | sourceTree = "";
64 | };
65 | 52EB7E492C70B90400069B79 /* Products */ = {
66 | isa = PBXGroup;
67 | children = (
68 | 52EB7E482C70B90400069B79 /* NetworkingLayerSwift6.app */,
69 | 5240D4D02C72A90E000F58CD /* NetworkingLayerSwift6Tests.xctest */,
70 | );
71 | name = Products;
72 | sourceTree = "";
73 | };
74 | /* End PBXGroup section */
75 |
76 | /* Begin PBXNativeTarget section */
77 | 5240D4CF2C72A90E000F58CD /* NetworkingLayerSwift6Tests */ = {
78 | isa = PBXNativeTarget;
79 | buildConfigurationList = 5240D4D82C72A90E000F58CD /* Build configuration list for PBXNativeTarget "NetworkingLayerSwift6Tests" */;
80 | buildPhases = (
81 | 5240D4CC2C72A90E000F58CD /* Sources */,
82 | 5240D4CD2C72A90E000F58CD /* Frameworks */,
83 | 5240D4CE2C72A90E000F58CD /* Resources */,
84 | );
85 | buildRules = (
86 | );
87 | dependencies = (
88 | 5240D4D52C72A90E000F58CD /* PBXTargetDependency */,
89 | );
90 | name = NetworkingLayerSwift6Tests;
91 | packageProductDependencies = (
92 | );
93 | productName = NetworkingLayerSwift6Tests;
94 | productReference = 5240D4D02C72A90E000F58CD /* NetworkingLayerSwift6Tests.xctest */;
95 | productType = "com.apple.product-type.bundle.unit-test";
96 | };
97 | 52EB7E472C70B90400069B79 /* NetworkingLayerSwift6 */ = {
98 | isa = PBXNativeTarget;
99 | buildConfigurationList = 52EB7E562C70B90500069B79 /* Build configuration list for PBXNativeTarget "NetworkingLayerSwift6" */;
100 | buildPhases = (
101 | 52EB7E442C70B90400069B79 /* Sources */,
102 | 52EB7E452C70B90400069B79 /* Frameworks */,
103 | 52EB7E462C70B90400069B79 /* Resources */,
104 | );
105 | buildRules = (
106 | );
107 | dependencies = (
108 | );
109 | fileSystemSynchronizedGroups = (
110 | 52EB7E4A2C70B90400069B79 /* NetworkingLayerSwift6 */,
111 | );
112 | name = NetworkingLayerSwift6;
113 | packageProductDependencies = (
114 | 522AB0D92D88E72000D733F0 /* EventHorizon */,
115 | 5206A5402D8F714C00E393D8 /* EventHorizon */,
116 | );
117 | productName = NetworkingLayerSwift6;
118 | productReference = 52EB7E482C70B90400069B79 /* NetworkingLayerSwift6.app */;
119 | productType = "com.apple.product-type.application";
120 | };
121 | /* End PBXNativeTarget section */
122 |
123 | /* Begin PBXProject section */
124 | 52EB7E402C70B90400069B79 /* Project object */ = {
125 | isa = PBXProject;
126 | attributes = {
127 | BuildIndependentTargetsInParallel = 1;
128 | LastSwiftUpdateCheck = 1610;
129 | LastUpgradeCheck = 1620;
130 | TargetAttributes = {
131 | 5240D4CF2C72A90E000F58CD = {
132 | CreatedOnToolsVersion = 16.1;
133 | TestTargetID = 52EB7E472C70B90400069B79;
134 | };
135 | 52EB7E472C70B90400069B79 = {
136 | CreatedOnToolsVersion = 16.1;
137 | };
138 | };
139 | };
140 | buildConfigurationList = 52EB7E432C70B90400069B79 /* Build configuration list for PBXProject "NetworkingLayerSwift6" */;
141 | developmentRegion = en;
142 | hasScannedForEncodings = 0;
143 | knownRegions = (
144 | en,
145 | Base,
146 | );
147 | mainGroup = 52EB7E3F2C70B90400069B79;
148 | minimizedProjectReferenceProxies = 1;
149 | packageReferences = (
150 | 5206A53F2D8F714C00E393D8 /* XCRemoteSwiftPackageReference "EventHorizon" */,
151 | );
152 | preferredProjectObjectVersion = 77;
153 | productRefGroup = 52EB7E492C70B90400069B79 /* Products */;
154 | projectDirPath = "";
155 | projectRoot = "";
156 | targets = (
157 | 52EB7E472C70B90400069B79 /* NetworkingLayerSwift6 */,
158 | 5240D4CF2C72A90E000F58CD /* NetworkingLayerSwift6Tests */,
159 | );
160 | };
161 | /* End PBXProject section */
162 |
163 | /* Begin PBXResourcesBuildPhase section */
164 | 5240D4CE2C72A90E000F58CD /* Resources */ = {
165 | isa = PBXResourcesBuildPhase;
166 | buildActionMask = 2147483647;
167 | files = (
168 | );
169 | runOnlyForDeploymentPostprocessing = 0;
170 | };
171 | 52EB7E462C70B90400069B79 /* Resources */ = {
172 | isa = PBXResourcesBuildPhase;
173 | buildActionMask = 2147483647;
174 | files = (
175 | );
176 | runOnlyForDeploymentPostprocessing = 0;
177 | };
178 | /* End PBXResourcesBuildPhase section */
179 |
180 | /* Begin PBXSourcesBuildPhase section */
181 | 5240D4CC2C72A90E000F58CD /* Sources */ = {
182 | isa = PBXSourcesBuildPhase;
183 | buildActionMask = 2147483647;
184 | files = (
185 | );
186 | runOnlyForDeploymentPostprocessing = 0;
187 | };
188 | 52EB7E442C70B90400069B79 /* Sources */ = {
189 | isa = PBXSourcesBuildPhase;
190 | buildActionMask = 2147483647;
191 | files = (
192 | );
193 | runOnlyForDeploymentPostprocessing = 0;
194 | };
195 | /* End PBXSourcesBuildPhase section */
196 |
197 | /* Begin PBXTargetDependency section */
198 | 5240D4D52C72A90E000F58CD /* PBXTargetDependency */ = {
199 | isa = PBXTargetDependency;
200 | target = 52EB7E472C70B90400069B79 /* NetworkingLayerSwift6 */;
201 | targetProxy = 5240D4D42C72A90E000F58CD /* PBXContainerItemProxy */;
202 | };
203 | /* End PBXTargetDependency section */
204 |
205 | /* Begin XCBuildConfiguration section */
206 | 5240D4D62C72A90E000F58CD /* Debug */ = {
207 | isa = XCBuildConfiguration;
208 | buildSettings = {
209 | BUNDLE_LOADER = "$(TEST_HOST)";
210 | CODE_SIGN_STYLE = Automatic;
211 | CURRENT_PROJECT_VERSION = 1;
212 | DEAD_CODE_STRIPPING = YES;
213 | DEVELOPMENT_TEAM = YUQDS8QJFD;
214 | GENERATE_INFOPLIST_FILE = YES;
215 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
216 | MACOSX_DEPLOYMENT_TARGET = 14.5;
217 | MARKETING_VERSION = 1.0;
218 | PRODUCT_BUNDLE_IDENTIFIER = egzonpllana.NetworkingLayerSwift6Tests;
219 | PRODUCT_NAME = "$(TARGET_NAME)";
220 | SDKROOT = auto;
221 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
222 | SWIFT_EMIT_LOC_STRINGS = NO;
223 | SWIFT_VERSION = 5.0;
224 | TARGETED_DEVICE_FAMILY = "1,2,7";
225 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NetworkingLayerSwift6.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NetworkingLayerSwift6";
226 | XROS_DEPLOYMENT_TARGET = 2.0;
227 | };
228 | name = Debug;
229 | };
230 | 5240D4D72C72A90E000F58CD /* Release */ = {
231 | isa = XCBuildConfiguration;
232 | buildSettings = {
233 | BUNDLE_LOADER = "$(TEST_HOST)";
234 | CODE_SIGN_STYLE = Automatic;
235 | CURRENT_PROJECT_VERSION = 1;
236 | DEAD_CODE_STRIPPING = YES;
237 | DEVELOPMENT_TEAM = YUQDS8QJFD;
238 | GENERATE_INFOPLIST_FILE = YES;
239 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
240 | MACOSX_DEPLOYMENT_TARGET = 14.5;
241 | MARKETING_VERSION = 1.0;
242 | PRODUCT_BUNDLE_IDENTIFIER = egzonpllana.NetworkingLayerSwift6Tests;
243 | PRODUCT_NAME = "$(TARGET_NAME)";
244 | SDKROOT = auto;
245 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
246 | SWIFT_EMIT_LOC_STRINGS = NO;
247 | SWIFT_VERSION = 5.0;
248 | TARGETED_DEVICE_FAMILY = "1,2,7";
249 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NetworkingLayerSwift6.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NetworkingLayerSwift6";
250 | XROS_DEPLOYMENT_TARGET = 2.0;
251 | };
252 | name = Release;
253 | };
254 | 52EB7E542C70B90500069B79 /* Debug */ = {
255 | isa = XCBuildConfiguration;
256 | buildSettings = {
257 | ALWAYS_SEARCH_USER_PATHS = NO;
258 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
259 | CLANG_ANALYZER_NONNULL = YES;
260 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
261 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
262 | CLANG_ENABLE_MODULES = YES;
263 | CLANG_ENABLE_OBJC_ARC = YES;
264 | CLANG_ENABLE_OBJC_WEAK = YES;
265 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
266 | CLANG_WARN_BOOL_CONVERSION = YES;
267 | CLANG_WARN_COMMA = YES;
268 | CLANG_WARN_CONSTANT_CONVERSION = YES;
269 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
270 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
271 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
272 | CLANG_WARN_EMPTY_BODY = YES;
273 | CLANG_WARN_ENUM_CONVERSION = YES;
274 | CLANG_WARN_INFINITE_RECURSION = YES;
275 | CLANG_WARN_INT_CONVERSION = YES;
276 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
277 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
278 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
279 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
280 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
281 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
282 | CLANG_WARN_STRICT_PROTOTYPES = YES;
283 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
284 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
285 | CLANG_WARN_UNREACHABLE_CODE = YES;
286 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
287 | COPY_PHASE_STRIP = NO;
288 | DEBUG_INFORMATION_FORMAT = dwarf;
289 | ENABLE_STRICT_OBJC_MSGSEND = YES;
290 | ENABLE_TESTABILITY = YES;
291 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
292 | GCC_C_LANGUAGE_STANDARD = gnu17;
293 | GCC_DYNAMIC_NO_PIC = NO;
294 | GCC_NO_COMMON_BLOCKS = YES;
295 | GCC_OPTIMIZATION_LEVEL = 0;
296 | GCC_PREPROCESSOR_DEFINITIONS = (
297 | "DEBUG=1",
298 | "$(inherited)",
299 | );
300 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
301 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
302 | GCC_WARN_UNDECLARED_SELECTOR = YES;
303 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
304 | GCC_WARN_UNUSED_FUNCTION = YES;
305 | GCC_WARN_UNUSED_VARIABLE = YES;
306 | IPHONEOS_DEPLOYMENT_TARGET = 18.1;
307 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
308 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
309 | MTL_FAST_MATH = YES;
310 | ONLY_ACTIVE_ARCH = YES;
311 | SDKROOT = iphoneos;
312 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
313 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
314 | };
315 | name = Debug;
316 | };
317 | 52EB7E552C70B90500069B79 /* Release */ = {
318 | isa = XCBuildConfiguration;
319 | buildSettings = {
320 | ALWAYS_SEARCH_USER_PATHS = NO;
321 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
322 | CLANG_ANALYZER_NONNULL = YES;
323 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
324 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
325 | CLANG_ENABLE_MODULES = YES;
326 | CLANG_ENABLE_OBJC_ARC = YES;
327 | CLANG_ENABLE_OBJC_WEAK = YES;
328 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
329 | CLANG_WARN_BOOL_CONVERSION = YES;
330 | CLANG_WARN_COMMA = YES;
331 | CLANG_WARN_CONSTANT_CONVERSION = YES;
332 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
333 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
334 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
335 | CLANG_WARN_EMPTY_BODY = YES;
336 | CLANG_WARN_ENUM_CONVERSION = YES;
337 | CLANG_WARN_INFINITE_RECURSION = YES;
338 | CLANG_WARN_INT_CONVERSION = YES;
339 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
340 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
341 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
342 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
343 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
344 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
345 | CLANG_WARN_STRICT_PROTOTYPES = YES;
346 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
347 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
348 | CLANG_WARN_UNREACHABLE_CODE = YES;
349 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
350 | COPY_PHASE_STRIP = NO;
351 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
352 | ENABLE_NS_ASSERTIONS = NO;
353 | ENABLE_STRICT_OBJC_MSGSEND = YES;
354 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
355 | GCC_C_LANGUAGE_STANDARD = gnu17;
356 | GCC_NO_COMMON_BLOCKS = YES;
357 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
358 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
359 | GCC_WARN_UNDECLARED_SELECTOR = YES;
360 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
361 | GCC_WARN_UNUSED_FUNCTION = YES;
362 | GCC_WARN_UNUSED_VARIABLE = YES;
363 | IPHONEOS_DEPLOYMENT_TARGET = 18.1;
364 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
365 | MTL_ENABLE_DEBUG_INFO = NO;
366 | MTL_FAST_MATH = YES;
367 | SDKROOT = iphoneos;
368 | SWIFT_COMPILATION_MODE = wholemodule;
369 | VALIDATE_PRODUCT = YES;
370 | };
371 | name = Release;
372 | };
373 | 52EB7E572C70B90500069B79 /* Debug */ = {
374 | isa = XCBuildConfiguration;
375 | buildSettings = {
376 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
377 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
378 | CODE_SIGN_STYLE = Automatic;
379 | CURRENT_PROJECT_VERSION = 1;
380 | DEVELOPMENT_ASSET_PATHS = "\"NetworkingLayerSwift6/Preview Content\"";
381 | DEVELOPMENT_TEAM = YUQDS8QJFD;
382 | ENABLE_PREVIEWS = YES;
383 | GENERATE_INFOPLIST_FILE = YES;
384 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
385 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
386 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
387 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
388 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
389 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
390 | LD_RUNPATH_SEARCH_PATHS = (
391 | "$(inherited)",
392 | "@executable_path/Frameworks",
393 | );
394 | MARKETING_VERSION = 1.0;
395 | PRODUCT_BUNDLE_IDENTIFIER = egzonpllana.NetworkingLayerSwift6;
396 | PRODUCT_NAME = "$(TARGET_NAME)";
397 | SWIFT_EMIT_LOC_STRINGS = YES;
398 | SWIFT_STRICT_CONCURRENCY = complete;
399 | SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES;
400 | SWIFT_VERSION = 6.0;
401 | TARGETED_DEVICE_FAMILY = "1,2";
402 | };
403 | name = Debug;
404 | };
405 | 52EB7E582C70B90500069B79 /* Release */ = {
406 | isa = XCBuildConfiguration;
407 | buildSettings = {
408 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
409 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
410 | CODE_SIGN_STYLE = Automatic;
411 | CURRENT_PROJECT_VERSION = 1;
412 | DEVELOPMENT_ASSET_PATHS = "\"NetworkingLayerSwift6/Preview Content\"";
413 | DEVELOPMENT_TEAM = YUQDS8QJFD;
414 | ENABLE_PREVIEWS = YES;
415 | GENERATE_INFOPLIST_FILE = YES;
416 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
417 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
418 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
419 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
420 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
421 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
422 | LD_RUNPATH_SEARCH_PATHS = (
423 | "$(inherited)",
424 | "@executable_path/Frameworks",
425 | );
426 | MARKETING_VERSION = 1.0;
427 | PRODUCT_BUNDLE_IDENTIFIER = egzonpllana.NetworkingLayerSwift6;
428 | PRODUCT_NAME = "$(TARGET_NAME)";
429 | SWIFT_EMIT_LOC_STRINGS = YES;
430 | SWIFT_STRICT_CONCURRENCY = complete;
431 | SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES;
432 | SWIFT_VERSION = 6.0;
433 | TARGETED_DEVICE_FAMILY = "1,2";
434 | };
435 | name = Release;
436 | };
437 | /* End XCBuildConfiguration section */
438 |
439 | /* Begin XCConfigurationList section */
440 | 5240D4D82C72A90E000F58CD /* Build configuration list for PBXNativeTarget "NetworkingLayerSwift6Tests" */ = {
441 | isa = XCConfigurationList;
442 | buildConfigurations = (
443 | 5240D4D62C72A90E000F58CD /* Debug */,
444 | 5240D4D72C72A90E000F58CD /* Release */,
445 | );
446 | defaultConfigurationIsVisible = 0;
447 | defaultConfigurationName = Release;
448 | };
449 | 52EB7E432C70B90400069B79 /* Build configuration list for PBXProject "NetworkingLayerSwift6" */ = {
450 | isa = XCConfigurationList;
451 | buildConfigurations = (
452 | 52EB7E542C70B90500069B79 /* Debug */,
453 | 52EB7E552C70B90500069B79 /* Release */,
454 | );
455 | defaultConfigurationIsVisible = 0;
456 | defaultConfigurationName = Release;
457 | };
458 | 52EB7E562C70B90500069B79 /* Build configuration list for PBXNativeTarget "NetworkingLayerSwift6" */ = {
459 | isa = XCConfigurationList;
460 | buildConfigurations = (
461 | 52EB7E572C70B90500069B79 /* Debug */,
462 | 52EB7E582C70B90500069B79 /* Release */,
463 | );
464 | defaultConfigurationIsVisible = 0;
465 | defaultConfigurationName = Release;
466 | };
467 | /* End XCConfigurationList section */
468 |
469 | /* Begin XCRemoteSwiftPackageReference section */
470 | 5206A53F2D8F714C00E393D8 /* XCRemoteSwiftPackageReference "EventHorizon" */ = {
471 | isa = XCRemoteSwiftPackageReference;
472 | repositoryURL = "https://github.com/egzonpllana/EventHorizon.git";
473 | requirement = {
474 | kind = upToNextMajorVersion;
475 | minimumVersion = 1.0.0;
476 | };
477 | };
478 | /* End XCRemoteSwiftPackageReference section */
479 |
480 | /* Begin XCSwiftPackageProductDependency section */
481 | 5206A5402D8F714C00E393D8 /* EventHorizon */ = {
482 | isa = XCSwiftPackageProductDependency;
483 | package = 5206A53F2D8F714C00E393D8 /* XCRemoteSwiftPackageReference "EventHorizon" */;
484 | productName = EventHorizon;
485 | };
486 | 522AB0D92D88E72000D733F0 /* EventHorizon */ = {
487 | isa = XCSwiftPackageProductDependency;
488 | productName = EventHorizon;
489 | };
490 | /* End XCSwiftPackageProductDependency section */
491 | };
492 | rootObject = 52EB7E402C70B90400069B79 /* Project object */;
493 | }
494 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6.xcodeproj/project.xcworkspace/xcuserdata/egzonpllana.xcuserdatad/IDEFindNavigatorScopes.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6.xcodeproj/xcuserdata/egzonpllana.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6.xcodeproj/xcuserdata/egzonpllana.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | NetworkingLayerSwift6.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Core/API/APIEndpoints.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIEndpoint.swift
3 | //
4 | // Created by Egzon Pllana.
5 | //
6 |
7 | import Foundation
8 | import EventHorizon
9 |
10 | private enum Constants {
11 | static let baseURL = "https://jsonplaceholder.typicode.com"
12 | static let uploadPath = "upload"
13 | static let postPath = "posts"
14 | static let contentTypeHeader = "Content-Type"
15 | }
16 |
17 | // Endpoints
18 | enum APIEndpointExample {
19 | case getPosts
20 | case createPost(PostDTO)
21 | case uploadImage(data: Data, fileName: String, mimeType: ImageMimeType)
22 | }
23 |
24 | /// Extension to conform to `APIEndpointProtocol`.
25 | extension APIEndpointExample: APIEndpointProtocol {
26 |
27 | var apiVersion: String {
28 | APIVersion.v1.rawValue
29 | }
30 |
31 | /// Endpoint base URL.
32 | var baseURL: String {
33 | return Constants.baseURL
34 | }
35 |
36 | /// Endpoint HTTP method.
37 | var method: HTTPMethod {
38 | switch self {
39 | case .getPosts:
40 | return .get
41 | case .createPost, .uploadImage:
42 | return .post
43 | }
44 | }
45 |
46 | /// Endpoint path.
47 | var path: String {
48 | switch self {
49 | case .getPosts, .createPost:
50 | return Constants.postPath
51 | case .uploadImage:
52 | return Constants.uploadPath
53 | }
54 | }
55 |
56 | /// Request headers.
57 | var headers: [String: String] {
58 | guard let body = body else { return [:] }
59 | return [Constants.contentTypeHeader: body.contentType]
60 | }
61 |
62 | /// Request URL parameters.
63 | var urlParams: [String: any CustomStringConvertible] {
64 | return [:]
65 | }
66 |
67 | /// Request body data.
68 | var body: HTTPBody? {
69 | switch self {
70 | case .createPost(let postDTO):
71 | guard let postData = postDTO.toJSONData() else {
72 | return nil
73 | }
74 | return .data(postData)
75 | case .uploadImage(let data, let fileName, let mimeType):
76 | let multipartData = MultipartFormData(
77 | boundary: UUID().uuidString,
78 | fileData: data,
79 | fileName: fileName,
80 | mimeType: mimeType.asString,
81 | parameters: [:]
82 | )
83 | return .multipartFormData(multipartData)
84 | case .getPosts:
85 | return nil
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Core/API/APIVersion.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIVersion.swift
3 | //
4 | // Created by Egzon Pllana.
5 | //
6 |
7 | import Foundation
8 |
9 | /// Enum defining different versions of the API.
10 | enum APIVersion: String {
11 | /// Version 1 of the API.
12 | case v1 = "/api/v1/"
13 | }
14 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Core/API/Interceptors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Interceptors.swift
3 | // NetworkingLayerSwift6
4 | //
5 | // Created by Egzon Pllana on 15.3.25.
6 | //
7 |
8 | import EventHorizon
9 |
10 | enum Interceptors {
11 | static let example: [any NetworkInterceptorProtocol] = [
12 | AuthInterceptor(tokenProvider: "my_token"),
13 | LoggingInterceptor(),
14 | RetryInterceptor(),
15 | RequestTimeoutInterceptor(timeout: 10),
16 | HeaderInjectorInterceptor(headers: ["User-Agent": "MyApp/1.0"])
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Data/Entities/PostDTO.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostDTO.swift
3 | //
4 | // Created by Egzon Pllana.
5 | //
6 |
7 | import Foundation
8 |
9 | /// A struct representing the data transfer object for a post.
10 | struct PostDTO: Codable {
11 | let userId: Int
12 | let title: String
13 | let body: String
14 | }
15 |
16 | extension PostDTO {
17 | /// Converts the `PostDTO` data to JSON format for use in a request body.
18 | ///
19 | /// - Returns: A `Data` object representing the post data in JSON format, or `nil` if the conversion fails.
20 | func toJSONData() -> Data? {
21 | let jsonObject: [String: Any] = [
22 | "userId": userId,
23 | "title": title,
24 | "body": body
25 | ]
26 | return try? JSONSerialization.data(withJSONObject: jsonObject, options: [])
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Data/PostsRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostsRepository.swift
3 | //
4 | // Created by Egzon Pllana.
5 | //
6 |
7 | import UIKit
8 | import EventHorizon
9 |
10 | enum PostConstants {
11 | static let defaultUserId = 1
12 | static let defaultTitle = "Title here"
13 | static let defaultBody = "Body here"
14 | static let smallImageName = "image-png.png"
15 | static let imageName = "image-name"
16 | }
17 |
18 | /// Concrete implementation of `PostsRepositoryProtocol` for managing posts and image uploads.
19 | final class PostsRepository: PostsRepositoryProtocol {
20 | private let apiClient: any APIClientProtocol
21 | private typealias apiEndpoint = APIEndpointExample
22 |
23 | init(
24 | apiClient: any APIClientProtocol = APIClient(
25 | interceptors: Interceptors.example
26 | )
27 | ) {
28 | self.apiClient = apiClient
29 | }
30 |
31 | func getPosts() async throws -> [PostDTO] {
32 | let fetchedPosts: [PostDTO] = try await apiClient.request(apiEndpoint.getPosts)
33 | return fetchedPosts
34 | }
35 |
36 | func createPost() async throws {
37 | let newPost = PostDTO(
38 | userId: PostConstants.defaultUserId,
39 | title: PostConstants.defaultTitle,
40 | body: PostConstants.defaultBody
41 | )
42 | do {
43 | try await apiClient.request(apiEndpoint.createPost(newPost))
44 | } catch {
45 | log("Error: \(error)")
46 | throw error
47 | }
48 | }
49 |
50 | func uploadImage(
51 | data: Data,
52 | fileName: String,
53 | mimeType: String,
54 | progressDelegate: (any UploadProgressDelegateProtocol)? = nil
55 | ) async throws {
56 | let endPoint = apiEndpoint.uploadImage(
57 | data: data,
58 | fileName: fileName,
59 | mimeType: ImageMimeType(rawValue: mimeType) ?? .png
60 | )
61 | do {
62 | try await apiClient.request(
63 | endPoint,
64 | progressDelegate: progressDelegate
65 | )
66 | } catch {
67 | log("Error uploading image: \(error)")
68 | throw error
69 | }
70 | }
71 |
72 | private func log(_ string: String) {
73 | #if DEBUG
74 | print(string)
75 | #endif
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Domain/Protocols/PostsRepositoryProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostsRepositoryProtocol.swift
3 | //
4 | // Created by Egzon Pllana.
5 | //
6 |
7 | import Foundation
8 | import EventHorizon
9 |
10 | /// Protocol defining the operations for managing posts and uploading images.
11 | protocol PostsRepositoryProtocol: Sendable {
12 | /// Fetches posts from the API.
13 | /// - Returns: An array of `PostDTO` objects.
14 | /// - Throws: An error if the request fails.
15 | func getPosts() async throws -> [PostDTO]
16 |
17 | /// Creates a new post with default values.
18 | /// - Throws: An error if the request fails.
19 | func createPost() async throws
20 |
21 | func uploadImage(
22 | data: Data,
23 | fileName: String,
24 | mimeType: String,
25 | progressDelegate: (any UploadProgressDelegateProtocol)?
26 | ) async throws
27 | }
28 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Domain/Protocols/PostsRepositoryUseCaseProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostsRepositoryUseCaseProtocol.swift
3 | //
4 | // Created by Egzon Pllana.
5 | //
6 |
7 | import Foundation
8 | import EventHorizon
9 |
10 | /// Protocol defining the use case for managing post-related operations.
11 | protocol PostsRepositoryUseCaseProtocol: Sendable {
12 | /// Retrieves posts using the `PostsRepository`.
13 | /// - Returns: An array of `PostDTO` objects.
14 | /// - Throws: An error if the service request fails.
15 | func getPosts() async throws -> [PostDTO]
16 |
17 | /// Creates a new post using the `PostsRepository`.
18 | /// - Throws: An error if the service request fails.
19 | func createPost() async throws
20 |
21 | func uploadImage(
22 | data: Data,
23 | fileName: String,
24 | mimeType: String,
25 | progressDelegate: (any UploadProgressDelegateProtocol)?
26 | ) async throws
27 | }
28 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Domain/UseCases/PostsRepositoryUseCase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostsRepositoryUseCase.swift
3 | //
4 | // Created by Egzon Pllana.
5 | //
6 |
7 | import Foundation
8 | import EventHorizon
9 |
10 | /// Concrete implementation of `PostsRepositoryUseCaseProtocol` for managing post-related operations.
11 | final class PostsRepositoryUseCase: PostsRepositoryUseCaseProtocol {
12 |
13 | private let postsRepository: any PostsRepositoryProtocol
14 |
15 | init(postsRepository: any PostsRepositoryProtocol = PostsRepository()) {
16 | self.postsRepository = postsRepository
17 | }
18 |
19 | func getPosts() async throws -> [PostDTO] {
20 | return try await postsRepository.getPosts()
21 | }
22 |
23 | func createPost() async throws {
24 | try await postsRepository.createPost()
25 | }
26 |
27 | func uploadImage(data: Data, fileName: String, mimeType: String, progressDelegate: (any UploadProgressDelegateProtocol)?) async throws {
28 | try await postsRepository.uploadImage(
29 | data: data,
30 | fileName: fileName,
31 | mimeType: mimeType,
32 | progressDelegate: progressDelegate
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/NetworkingLayerSwift6App.swift:
--------------------------------------------------------------------------------
1 | /*
2 | MIT License
3 |
4 | Copyright (c) 2024 Egzon Pllana
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 |
25 | import SwiftUI
26 |
27 | @main
28 | struct NetworkingLayerSwift6App: App {
29 |
30 | var body: some Scene {
31 | WindowGroup {
32 | HomeView()
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Presentation/Home/HomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | //
4 | // Created by Egzon Pllana.
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct HomeView: View {
10 |
11 | // MARK: - Properties -
12 | @StateObject private var viewModel = HomeViewModel()
13 |
14 | // MARK: - View -
15 | var body: some View {
16 | VStack {
17 | Image(systemName: "globe")
18 | .imageScale(.large)
19 | VStack {
20 | Text("Posts: \(viewModel.posts.count)")
21 | Text("Upload progress: \(String(format: "%.0f%%", viewModel.uploadProgress * 100))")
22 | }
23 | .padding()
24 | }
25 | .padding()
26 | .onAppear {
27 | getPosts()
28 | createPost()
29 | uploadImage()
30 | }
31 | }
32 |
33 | // MARK: - Methods -
34 | private func getPosts() {
35 | Task {
36 | try await viewModel.getPosts()
37 | print("[Task] Get posts finished.")
38 | }
39 | }
40 |
41 | private func createPost() {
42 | Task {
43 | try await viewModel.createPost()
44 | print("[Task] Create post finished.")
45 | }
46 | }
47 |
48 | private func uploadImage() {
49 | Task {
50 | try await viewModel.uploadImage()
51 | print("[Task] Upload image finished.")
52 | }
53 | }
54 | }
55 |
56 | #Preview {
57 | HomeView()
58 | }
59 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Presentation/Home/HomeViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewModel.swift
3 | //
4 | // Created by Egzon Pllana.
5 | //
6 |
7 | import UIKit
8 | import EventHorizon
9 |
10 | final class HomeViewModel: HomeViewModelProtocol {
11 |
12 | // MARK: - Publishers -
13 | @Published var posts: [PostDTO] = []
14 | @Published var uploadProgress: Double = 0.0
15 |
16 | // MARK: - Properties -
17 | private let postsRepositoryUseCase: any PostsRepositoryUseCaseProtocol
18 |
19 | // MARK: - Initialization -
20 | init(
21 | postsRepositoryUseCase: any PostsRepositoryUseCaseProtocol = PostsRepositoryUseCase()
22 | ) {
23 | self.postsRepositoryUseCase = postsRepositoryUseCase
24 | }
25 |
26 | // MARK: - Methods -
27 | @discardableResult
28 | func getPosts() async throws -> [PostDTO] {
29 | self.posts = try await postsRepositoryUseCase.getPosts()
30 | return posts
31 | }
32 |
33 | func createPost() async throws {
34 | try await postsRepositoryUseCase.createPost()
35 | }
36 |
37 | func uploadImage() async throws {
38 | guard let image = UIImage(named: PostConstants.smallImageName),
39 | let data = image.jpegData(compressionQuality: 1.0) else {
40 | return
41 | }
42 |
43 | // Create the progress delegate inline.
44 | let progressDelegate = UploadProgressDelegate { [weak self] progress in
45 | // Update progress on the main thread.
46 | Task { @MainActor in
47 | self?.uploadProgress = progress
48 | }
49 | }
50 |
51 | // Pass the progress delegate to the upload method.
52 | // Note: current free API that we use do not support uploading multi part data, it will report the 503 status code.
53 | try await postsRepositoryUseCase.uploadImage(
54 | data: data,
55 | fileName: PostConstants.imageName,
56 | mimeType: ImageMimeType.png.asString,
57 | progressDelegate: progressDelegate
58 | )
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Architecture/Presentation/Home/HomeViewModelProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewModelProtocol.swift
3 | //
4 | // Created by Egzon Pllana.
5 | //
6 |
7 | import Foundation
8 |
9 | /// A protocol defining the interface for a Home ViewModel.
10 | ///
11 | /// The `HomeViewModelProtocol` provides properties and methods for managing posts and uploading images.
12 | ///
13 | /// - Conforms to: `MainActor`
14 | /// - Requires: `ObservableObject` to support SwiftUI's data-binding and reactivity.
15 | @MainActor
16 | protocol HomeViewModelProtocol: ObservableObject, Sendable {
17 |
18 | /// A list of posts managed by the ViewModel.
19 | ///
20 | /// This property provides the current collection of posts.
21 | /// It is expected to be updated as posts are fetched or created.
22 | var posts: [PostDTO] { get }
23 |
24 | /// A read-only computed property for the upload progress.
25 | var uploadProgress: Double { get }
26 |
27 | /// Fetches a list of posts from the API.
28 | ///
29 | /// This method asynchronously retrieves posts from the API and returns them.
30 | ///
31 | /// - Returns: An array of `PostDTO` representing the fetched posts.
32 | /// - Throws: An error if the request fails or data cannot be parsed.
33 | @discardableResult
34 | func getPosts() async throws -> [PostDTO]
35 |
36 | /// Creates a new post.
37 | ///
38 | /// This method asynchronously creates a new post by sending data to the API.
39 | ///
40 | /// - Throws: An error if the creation request fails.
41 | func createPost() async throws
42 |
43 | /// Uploads an image to the API with progress tracking.
44 | ///
45 | /// This method asynchronously uploads an image to the API. It also supports progress tracking
46 | /// through a `progressDelegate`, if provided.
47 | ///
48 | /// - Throws: An error if the upload fails.
49 | func uploadImage() async throws
50 | }
51 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Resources/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "appearances" : [
10 | {
11 | "appearance" : "luminosity",
12 | "value" : "dark"
13 | }
14 | ],
15 | "idiom" : "universal",
16 | "platform" : "ios",
17 | "size" : "1024x1024"
18 | },
19 | {
20 | "appearances" : [
21 | {
22 | "appearance" : "luminosity",
23 | "value" : "tinted"
24 | }
25 | ],
26 | "idiom" : "universal",
27 | "platform" : "ios",
28 | "size" : "1024x1024"
29 | }
30 | ],
31 | "info" : {
32 | "author" : "xcode",
33 | "version" : 1
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/NetworkingLayerSwift6/Resources/image-png.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/egzonpllana/NetworkLayerSwift6/88d5be14d293f81e7b5e49826f11d7fa35c1cc65/NetworkingLayerSwift6/Resources/image-png.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Building a generic, thread-safe Networking Layer in Swift 6 with Interceptors
2 |
3 | 
4 |
5 | ___
6 | #### Built by using EventHorizon Swift package:
7 | https://github.com/egzonpllana/EventHorizon
8 |
9 | In this article, we will build a networking layer that meets the thread safety requirements introduced in **Swift 6**, using Swift features such as async-await, Sendable, MainActor, etc. While many of these features appeared in Swift 5.5, they are becoming more important, especially in Swift 6.
10 |
11 | To make sure your project aligns with these changes, select your project in Xcode, then go to your target’s Build Settings > under “All & Combined,” search for “Swift 6” or find the “Swift Compiler — Upcoming Features” section and enable these settings as you want and XCode will throw errors or warnings.
12 |
13 | > There will be no future in Swift development without async await and Sendable protocol.
14 |
15 | The networking layer we will build will use the latest Swift concurrency APIs and thread-safe methods to avoid multi-threading issues or crashes. The key components of this networking layer will be the APIEndpoint protocol and the APIClient protocol. This will let us seamlessly execute network calls and obtain results with just one line of code⚡️.
16 |
17 | ```swift
18 | // get
19 | let posts: [PostDTO] = try await apiClient.request(APIEndpoint.getPosts)
20 |
21 | // post
22 | let newPost: PostDTO = .init()
23 | try await apiClient.requestVoid(APIEndpoint.createPost(newPost))
24 |
25 | // multi-part request
26 | try await apiClient.requestWithProgress(APIEndpoint.uploadImage(...),
27 | progressDelegate: UploadProgressDelegateProtocol)
28 | ```
29 |
30 | #### APIEndpointProtocol
31 |
32 | APIEndpointProtocol defines the essential components of an API endpoint, such as HTTP methods, paths, base URLs, headers, URL parameters, and request bodies. It ensures a consistent and clear approach to constructing network requests through its urlRequest property, which assembles a URLRequest by combining these elements.
33 |
34 | ```swift
35 | protocol APIEndpointProtocol {
36 | /// HTTP method used by the endpoint.
37 | var method: HTTPMethod { get }
38 |
39 | /// Path for the endpoint.
40 | var path: String { get }
41 |
42 | /// Base URL for the API.
43 | var baseURL: String { get }
44 |
45 | /// Headers for the request.
46 | var headers: [String: String] { get }
47 |
48 | /// URL parameters for the request.
49 | var urlParams: [String: any CustomStringConvertible] { get }
50 |
51 | /// Body data for the request.
52 | var body: Data? { get }
53 |
54 | /// URLRequest representation of the endpoint.
55 | var urlRequest: URLRequest? { get }
56 |
57 | /// API version used by the endpoint.
58 | var apiVersion: APIVersion { get }
59 | }
60 |
61 | /// Endpoints
62 | enum APIEndpoint: APIEndpointProtocol {
63 | case getPosts
64 | case createPost(PostDTO)
65 | case uploadImage(data: Data, fileName: String, mimeType: ImageMimeType)
66 |
67 | // Define all properties required by the protocol,
68 | // matching your backend API.
69 | }
70 | ```
71 |
72 | #### APIClientProtocol
73 |
74 | APIClientProtocol defines the contract for making network requests and handling responses in a structured way. It abstracts away the complexity of sending HTTP requests and decoding responses, allowing you to focus on the data and logic. The protocol supports asynchronous operations and is designed to work with any type that conforms to Codable and Sendable.
75 |
76 | **Key methods include:**
77 |
78 | * `request(:decoder:)`: Sends a request using URLSession, decodes the response into a specified type, and returns the result.
79 |
80 | * `requestVoid(:)`: Sends a request that provides no response data.
81 |
82 | * `requestWithAlamofire(:decoder:)`: Sends a request using Alamofire and decodes the response.
83 |
84 | * `requestWithProgress(:progressDelegate:)`: Fetches raw data with optional upload progress tracking.
85 |
86 | ```swift
87 | func request(
88 | _ endpoint: any APIEndpointProtocol,
89 | decoder: JSONDecoder
90 | ) async throws -> T {
91 | guard let request = endpoint.urlRequest else {
92 | throw APIClientError.invalidURL
93 | }
94 |
95 | // Perform the network request and decode the data
96 | let data = try await performRequest(request)
97 | do {
98 | return try decoder.decode(T.self, from: data)
99 | } catch {
100 | // Handle decoding errors
101 | throw APIClientError.decodingFailed(error)
102 | }
103 | }
104 | ```
105 |
106 | **Parameters**
107 |
108 | * `endpoint` any APIEndpointProtocol: This parameter represents the API endpoint that contains the URL request configuration. The any keyword allows for any type that conforms to APIEndpointProtocol. This protocol typically includes properties or methods to provide the URL request needed for the network call.
109 |
110 | * `decoder`: JSONDecoder: This parameter is an instance of JSONDecoder, used to decode the JSON response into a Swift model. The JSONDecoder converts JSON data into instances of types that conform to the Decodable protocol.
111 |
112 |
113 | **Generic Type T**
114 |
115 | * `T: Decodable & Sendable`: is a generic type that must conform to both Decodable and Sendable protocols.Decodable: This protocol allows the type to be initialized from JSON data. It ensures that the type can be created from a serialized JSON format.
116 |
117 | * `Sendable`: This protocol indicates that the type can be safely used in concurrent code. It’s essential for types that will be used across different threads or tasks, ensuring that they don’t cause data races or concurrency issues.
118 |
119 |
120 | ### Network Interceptor
121 |
122 | #### What is a Network Interceptor?
123 | A **Network Interceptor** is a component that allows you to modify or inspect outgoing requests and incoming responses in a networking layer. It acts as a middleware between the `APIClient` and the actual network request execution.
124 |
125 | #### What it does
126 | Network Interceptors can:
127 | - Modify requests before they are sent (e.g., adding headers, authentication tokens).
128 | - Log requests and responses for debugging.
129 | - Retry failed requests automatically.
130 | - Enforce timeouts or custom error handling strategies.
131 |
132 | #### When to use
133 | Use Network Interceptors when you need to:
134 | - Standardize authentication by injecting tokens into every request.
135 | - Log network activity without modifying the `APIClient` logic.
136 | - Handle automatic retries for specific failure conditions.
137 | - Add request-specific configurations dynamically.
138 |
139 | #### Example: Adding Headers with a Network Interceptor
140 | A common use case for interceptors is injecting headers (such as authorization tokens or custom identifiers) into every request.
141 |
142 | ```swift
143 | struct HeaderInjectorInterceptor: NetworkInterceptor {
144 | private let headers: [String: String]
145 |
146 | init(headers: [String: String]) {
147 | self.headers = headers
148 | }
149 |
150 | func intercept(_ request: URLRequest) -> URLRequest {
151 | var modifiedRequest = request
152 | headers.forEach { key, value in
153 | modifiedRequest.setValue(value, forHTTPHeaderField: key)
154 | }
155 | return modifiedRequest
156 | }
157 | }
158 |
159 | // Usage in APIClient:
160 | let apiClient = APIClient(interceptors: [
161 | HeaderInjectorInterceptor(headers: ["Authorization": "Bearer my_token"])
162 | ])
163 | ```
164 |
165 | **Functionality**
166 |
167 | * **URL Request Validation**: The method first checks if the endpoint provides a valid URL request. If not, it throws an APIClientError.invalidURL, indicating a configuration issue.
168 |
169 | * **Perform Network Request**: It uses performRequest to execute the network call and retrieve the response data asynchronously. This method is likely defined elsewhere and handles the actual communication with the server.
170 |
171 | * **Decode Response Data**: The method attempts to decode the received data into the generic type T using the decoder. If decoding fails, it throws an APIClientError, providing details about the failure.
172 |
173 |
174 | The APIClient also has additional methods for different use cases
175 |
176 | * Request Void method, in cases when we only are interested in the request status (success or failure).
177 |
178 | * Request through Alamofire, which I do not recommend, but just in case you are a fan of our beloved framework from the past.
179 |
180 | * Request method that we want to get the request-response Data, in case we need to decode it differently.
181 |
182 |
183 | #### Real app implementation
184 |
185 | In this example, HomeViewModel leverages APIClient to handle network requests and update its posts property with the data fetched from an API.
186 | ```swift
187 | class HomeViewModel: ObservableObject {
188 | @Published var posts: [PostDTO] = []
189 | private let apiClient: APIClient = APIClient()
190 |
191 | func getPosts() async throws {
192 | posts = try await apiClient.request(APIEndpoint.getPosts)
193 | }
194 | }
195 | ```
196 | * The class is marked as `ObservableObject`, which allows SwiftUI views to observe changes in its properties.
197 |
198 | * The `@Published` modifier is used on the posts property so that the UI automatically updates whenever the value changes.
199 |
200 | * The `apiClient` is an instance of APIClient, which conforms to APIClientProtocol and handles all network interactions.
201 |
202 |
203 | The `getPosts()` method demonstrates how APIClient interacts with an API endpoint:
204 |
205 | * apiClient.request(APIEndpoint.getPosts) sends a request to the getPosts endpoint, using the `request(_:decoder:)` method from APIClientProtocol.
206 |
207 | * The result is decoded into an array of `PostDTO` and then assigned to the posts property.
208 |
209 | * This operation is performed asynchronously using Swift’s `async/await`, making it efficient and non-blocking.
210 |
211 |
212 | In this example, HomeView is a SwiftUI view that displays the number of posts fetched from an API using HomeViewModeland APIClient.
213 |
214 | ```swift
215 | struct HomeView: View {
216 | @StateObject private var viewModel = HomeViewModel()
217 |
218 | var body: some View {
219 | Text("Posts count: \(viewModel.posts.count)")
220 | }
221 | .onAppear {
222 | Task {
223 | try await viewModel.getPosts()
224 | }
225 | }
226 | }
227 | ```
228 |
229 | * HomeView uses `@StateObject` to create and manage the viewModel instance, which is responsible for handling data fetching.
230 |
231 | * The body of the view contains a simple Text element that displays the count of posts from the viewModel.
232 |
233 |
234 | In the `.onAppear` modifier:
235 |
236 | * A Task block is created to perform the asynchronous viewModel.getPosts() call. This ensures that the posts are fetched when the view appears on the screen.
237 |
238 | * Inside the task, `viewModel.getPosts()` is called asynchronously, requesting the API to retrieve posts via the APIClient. The posts array in HomeViewModel is updated when the data is successfully fetched, and the UI reflects the new data automatically due to the @Published property.
239 |
240 | Isn’t this the most beautiful Networking layer you have ever seen? If yes, let's go an extra mile to understand concurrency and thread-safe techniques in Swift:
241 | https://medium.com/p/5ccfdc0ca2b6
242 |
243 | ### The end 🏁
244 |
245 | I hope you found this article both engaging and useful for your projects. Personally, I have successfully applied these techniques in my own projects and technical challenges without any issues. You can customize and extend the methods as needed while utilizing generics to maintain code efficiency. Asynchronous programming with Sendable and async/await is likely to become a standard practice in the near future for any Apple platform.
246 |
247 | Thank you for following along. I encourage you to share any feedback or suggestions you may have about this Networking Layer. Together, we can continue to enhance and refine it.
248 |
249 | ### Resources
250 |
251 | * [MainActor](https://developer.apple.com/documentation/Swift/MainActor?changes=__7) _by Apple_
252 | * [Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/) _by Apple_
253 | * [Sendable protocol](https://developer.apple.com/documentation/swift/sendable#) _by Apple_
254 |
255 | ### Medium article:
256 |
257 | https://medium.com/@egzonpllana/building-a-generic-thread-safe-networking-layer-in-swift-6-927fa1d0cce8
258 |
259 | ### Let’s Connect
260 |
261 | * LinkedIn: [https://www.linkedin.com/in/egzon-pllana](https://www.linkedin.com/in/egzon-pllana)
262 |
263 | * GitHub: [https://github.com/egzonpllana](https://github.com/egzonpllana)
264 |
--------------------------------------------------------------------------------