From e0ab64953b06b79303283198e9fbdf100d7734d3 Mon Sep 17 00:00:00 2001 From: ChristRm Date: Mon, 30 Sep 2024 09:28:25 +0200 Subject: [PATCH 1/2] Add files via upload Implement iOS Camera & Image Preview Application --- IDNowTest.xcodeproj/project.pbxproj | 537 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 115334 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../xcschemes/xcschememanagement.plist | 14 + IDNowTest/AppDelegate.swift | 36 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + IDNowTest/Assets.xcassets/Contents.json | 6 + IDNowTest/Base.lproj/LaunchScreen.storyboard | 25 + IDNowTest/Base.lproj/Main.storyboard | 61 ++ .../CameraViewController.swift | 41 ++ .../ImageViewController.swift | 153 +++++ .../ImageViewController.xib | 90 +++ .../ImageViewController/ImageViewModel.swift | 60 ++ IDNowTest/Info.plist | 31 + IDNowTest/Models/Product.swift | 15 + IDNowTest/SceneDelegate.swift | 51 ++ IDNowTest/Services/CameraService.swift | 89 +++ IDNowTestTests/ImageViewModelTests.swift | 178 ++++++ 21 files changed, 1432 insertions(+) create mode 100644 IDNowTest.xcodeproj/project.pbxproj create mode 100644 IDNowTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 IDNowTest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 IDNowTest.xcodeproj/project.xcworkspace/xcuserdata/kristianrusyn.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 IDNowTest.xcodeproj/xcuserdata/kristianrusyn.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 IDNowTest.xcodeproj/xcuserdata/kristianrusyn.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 IDNowTest/AppDelegate.swift create mode 100644 IDNowTest/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 IDNowTest/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 IDNowTest/Assets.xcassets/Contents.json create mode 100644 IDNowTest/Base.lproj/LaunchScreen.storyboard create mode 100644 IDNowTest/Base.lproj/Main.storyboard create mode 100644 IDNowTest/CameraViewController/CameraViewController.swift create mode 100644 IDNowTest/ImageViewController/ImageViewController.swift create mode 100644 IDNowTest/ImageViewController/ImageViewController.xib create mode 100644 IDNowTest/ImageViewController/ImageViewModel.swift create mode 100644 IDNowTest/Info.plist create mode 100644 IDNowTest/Models/Product.swift create mode 100644 IDNowTest/SceneDelegate.swift create mode 100644 IDNowTest/Services/CameraService.swift create mode 100644 IDNowTestTests/ImageViewModelTests.swift diff --git a/IDNowTest.xcodeproj/project.pbxproj b/IDNowTest.xcodeproj/project.pbxproj new file mode 100644 index 0000000..aeaf76b --- /dev/null +++ b/IDNowTest.xcodeproj/project.pbxproj @@ -0,0 +1,537 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 70; + objects = { + +/* Begin PBXBuildFile section */ + 4F73A58E2CA980C60061114F /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F73A58D2CA980C60061114F /* CameraService.swift */; }; + 4FD76C932CA035D300988490 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD76C922CA035D300988490 /* AppDelegate.swift */; }; + 4FD76C952CA035D300988490 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD76C942CA035D300988490 /* SceneDelegate.swift */; }; + 4FD76C972CA035D300988490 /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD76C962CA035D300988490 /* CameraViewController.swift */; }; + 4FD76C9A2CA035D300988490 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = 4FD76C992CA035D300988490 /* Base */; }; + 4FD76C9C2CA035D500988490 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4FD76C9B2CA035D500988490 /* Assets.xcassets */; }; + 4FD76C9F2CA035D500988490 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = 4FD76C9E2CA035D500988490 /* Base */; }; + 4FD76CA72CA03A2300988490 /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD76CA62CA03A2300988490 /* Product.swift */; }; + 4FD76CB12CA05F0500988490 /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD76CAF2CA05F0500988490 /* ImageViewController.swift */; }; + 4FD76CB22CA05F0500988490 /* ImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4FD76CB02CA05F0500988490 /* ImageViewController.xib */; }; + 4FD76CB42CA0B4A300988490 /* ImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD76CB32CA0B4A300988490 /* ImageViewModel.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 4F73A59D2CAA70760061114F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4FD76C872CA035D300988490 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4FD76C8E2CA035D300988490; + remoteInfo = IDNowTest; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 4F73A58D2CA980C60061114F /* CameraService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = ""; }; + 4F73A5992CAA70760061114F /* IDNowTestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IDNowTestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4FD76C8F2CA035D300988490 /* IDNowTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IDNowTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4FD76C922CA035D300988490 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 4FD76C942CA035D300988490 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 4FD76C962CA035D300988490 /* CameraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = ""; }; + 4FD76C992CA035D300988490 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 4FD76C9B2CA035D500988490 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4FD76C9E2CA035D500988490 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 4FD76CA02CA035D500988490 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4FD76CA62CA03A2300988490 /* Product.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Product.swift; sourceTree = ""; }; + 4FD76CAF2CA05F0500988490 /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = ""; }; + 4FD76CB02CA05F0500988490 /* ImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ImageViewController.xib; sourceTree = ""; }; + 4FD76CB32CA0B4A300988490 /* ImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewModel.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 4F73A59A2CAA70760061114F /* IDNowTestTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = IDNowTestTests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4F73A5962CAA70760061114F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4FD76C8C2CA035D300988490 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4F73A58C2CA980AB0061114F /* Services */ = { + isa = PBXGroup; + children = ( + 4F73A58D2CA980C60061114F /* CameraService.swift */, + ); + path = Services; + sourceTree = ""; + }; + 4F73A58F2CA98A5A0061114F /* Models */ = { + isa = PBXGroup; + children = ( + 4FD76CA62CA03A2300988490 /* Product.swift */, + ); + path = Models; + sourceTree = ""; + }; + 4F73A5922CA9C6450061114F /* CameraViewController */ = { + isa = PBXGroup; + children = ( + 4FD76C962CA035D300988490 /* CameraViewController.swift */, + ); + path = CameraViewController; + sourceTree = ""; + }; + 4FD76C862CA035D300988490 = { + isa = PBXGroup; + children = ( + 4FD76C912CA035D300988490 /* IDNowTest */, + 4F73A59A2CAA70760061114F /* IDNowTestTests */, + 4FD76C902CA035D300988490 /* Products */, + ); + sourceTree = ""; + }; + 4FD76C902CA035D300988490 /* Products */ = { + isa = PBXGroup; + children = ( + 4FD76C8F2CA035D300988490 /* IDNowTest.app */, + 4F73A5992CAA70760061114F /* IDNowTestTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 4FD76C912CA035D300988490 /* IDNowTest */ = { + isa = PBXGroup; + children = ( + 4F73A5922CA9C6450061114F /* CameraViewController */, + 4F73A58F2CA98A5A0061114F /* Models */, + 4F73A58C2CA980AB0061114F /* Services */, + 4FD76CAA2CA05ED200988490 /* ImageViewController */, + 4FD76C922CA035D300988490 /* AppDelegate.swift */, + 4FD76C942CA035D300988490 /* SceneDelegate.swift */, + 4FD76C982CA035D300988490 /* Main.storyboard */, + 4FD76C9B2CA035D500988490 /* Assets.xcassets */, + 4FD76C9D2CA035D500988490 /* LaunchScreen.storyboard */, + 4FD76CA02CA035D500988490 /* Info.plist */, + ); + path = IDNowTest; + sourceTree = ""; + }; + 4FD76CAA2CA05ED200988490 /* ImageViewController */ = { + isa = PBXGroup; + children = ( + 4FD76CAF2CA05F0500988490 /* ImageViewController.swift */, + 4FD76CB02CA05F0500988490 /* ImageViewController.xib */, + 4FD76CB32CA0B4A300988490 /* ImageViewModel.swift */, + ); + path = ImageViewController; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4F73A5982CAA70760061114F /* IDNowTestTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4F73A59F2CAA70760061114F /* Build configuration list for PBXNativeTarget "IDNowTestTests" */; + buildPhases = ( + 4F73A5952CAA70760061114F /* Sources */, + 4F73A5962CAA70760061114F /* Frameworks */, + 4F73A5972CAA70760061114F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4F73A59E2CAA70760061114F /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 4F73A59A2CAA70760061114F /* IDNowTestTests */, + ); + name = IDNowTestTests; + packageProductDependencies = ( + ); + productName = IDNowTestTests; + productReference = 4F73A5992CAA70760061114F /* IDNowTestTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4FD76C8E2CA035D300988490 /* IDNowTest */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4FD76CA32CA035D500988490 /* Build configuration list for PBXNativeTarget "IDNowTest" */; + buildPhases = ( + 4FD76C8B2CA035D300988490 /* Sources */, + 4FD76C8C2CA035D300988490 /* Frameworks */, + 4FD76C8D2CA035D300988490 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IDNowTest; + productName = IDNowTest; + productReference = 4FD76C8F2CA035D300988490 /* IDNowTest.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4FD76C872CA035D300988490 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1540; + TargetAttributes = { + 4F73A5982CAA70760061114F = { + CreatedOnToolsVersion = 16.0; + TestTargetID = 4FD76C8E2CA035D300988490; + }; + 4FD76C8E2CA035D300988490 = { + CreatedOnToolsVersion = 15.4; + }; + }; + }; + buildConfigurationList = 4FD76C8A2CA035D300988490 /* Build configuration list for PBXProject "IDNowTest" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4FD76C862CA035D300988490; + productRefGroup = 4FD76C902CA035D300988490 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4FD76C8E2CA035D300988490 /* IDNowTest */, + 4F73A5982CAA70760061114F /* IDNowTestTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4F73A5972CAA70760061114F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4FD76C8D2CA035D300988490 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4FD76C9C2CA035D500988490 /* Assets.xcassets in Resources */, + 4FD76C9F2CA035D500988490 /* Base in Resources */, + 4FD76CB22CA05F0500988490 /* ImageViewController.xib in Resources */, + 4FD76C9A2CA035D300988490 /* Base in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4F73A5952CAA70760061114F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4FD76C8B2CA035D300988490 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4FD76CB42CA0B4A300988490 /* ImageViewModel.swift in Sources */, + 4FD76C972CA035D300988490 /* CameraViewController.swift in Sources */, + 4FD76C932CA035D300988490 /* AppDelegate.swift in Sources */, + 4FD76C952CA035D300988490 /* SceneDelegate.swift in Sources */, + 4FD76CB12CA05F0500988490 /* ImageViewController.swift in Sources */, + 4FD76CA72CA03A2300988490 /* Product.swift in Sources */, + 4F73A58E2CA980C60061114F /* CameraService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 4F73A59E2CAA70760061114F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4FD76C8E2CA035D300988490 /* IDNowTest */; + targetProxy = 4F73A59D2CAA70760061114F /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 4FD76C982CA035D300988490 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4FD76C992CA035D300988490 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 4FD76C9D2CA035D500988490 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4FD76C9E2CA035D500988490 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 4F73A5A02CAA70760061114F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = swiftui.IDNowTestTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IDNowTest.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/IDNowTest"; + }; + name = Debug; + }; + 4F73A5A12CAA70760061114F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = swiftui.IDNowTestTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IDNowTest.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/IDNowTest"; + }; + name = Release; + }; + 4FD76CA12CA035D500988490 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 4FD76CA22CA035D500988490 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4FD76CA42CA035D500988490 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = WGY9Q4U92K; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = IDNowTest/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = swiftui.IDNowTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4FD76CA52CA035D500988490 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = WGY9Q4U92K; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = IDNowTest/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = swiftui.IDNowTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4F73A59F2CAA70760061114F /* Build configuration list for PBXNativeTarget "IDNowTestTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4F73A5A02CAA70760061114F /* Debug */, + 4F73A5A12CAA70760061114F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4FD76C8A2CA035D300988490 /* Build configuration list for PBXProject "IDNowTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4FD76CA12CA035D500988490 /* Debug */, + 4FD76CA22CA035D500988490 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4FD76CA32CA035D500988490 /* Build configuration list for PBXNativeTarget "IDNowTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4FD76CA42CA035D500988490 /* Debug */, + 4FD76CA52CA035D500988490 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4FD76C872CA035D300988490 /* Project object */; +} diff --git a/IDNowTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/IDNowTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/IDNowTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/IDNowTest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/IDNowTest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/IDNowTest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/IDNowTest.xcodeproj/project.xcworkspace/xcuserdata/kristianrusyn.xcuserdatad/UserInterfaceState.xcuserstate b/IDNowTest.xcodeproj/project.xcworkspace/xcuserdata/kristianrusyn.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..d215a580b371ec866860cfcf097dc5a34213136f GIT binary patch literal 115334 zcmeF42YeJo`}lWf%kA#%?e6uv9KE-cOAnnufY2fI7DIA@KuAIgRdk0Af`W+jmQX_n zMVf+wNK={}1Zh&GC<2P;f96U89wM)Ac=>()AB=Y-ce6XQ&ph9Go_S{H**;n6X*sze zA)gZ@0SJnq35H+^4Iy-`G}x7$la`s$tx{5Ua=$e6SI0`ZnOR*cW%lXsO3uwO5ahT; ziHh7Np`~k>E2WXQ&{2XT_)ZDANx3eST&L$c6Iwzftb~oQ6EQ?>q7G4)s7KT%8W0VM zMno*pm}o-85lx9^L`R}K(St}LTtq6-pBO-75?Mqpkw*+8h7+$4uM(q)F~kI7A~A({ zotQ=}C6*D(i50|3qL5fctR~hFn~3*_EyN+>Q{pq?FmZ%9N*p7O6Q2`b5hsXK#A)Jt z;yiJQxK7+4ej;uYzYxEY49SxMDUmW+nXE!qC99Fu$r@x$vKARg29aT83|X73M>ZrI zk+Eb`vKiTeY)Q5v+mY?b4rD)aAeluDB6G<+au_+B96`QHs^r_`OmY@Eo18<=CFhaz z$pz#>atXPdTtTiPSCebV4dh006ZsCgh1^OWCch?6kSEF07xGu~H}XFDJ0JiFG(ZbPU;$QO19lJq%7F5q5{Ly&K{L=2v;yrwd(a7V2Himq zkO*D^DZmBNK!1<{GC>Z=1w+9wFcQ21MuE{_E|>@Ag9TtASOgY>d{6+EfTdt1SPwRU ztzaA24t9e*;A5~C>;uQZaqv0#0(=8bg73i@a2Z?ySHU%krWlH)G!#eilt5`IkL+YE$*8SgJAAl4?bzP%bK!>Pz*b(y0E_04kjtNM%sj zWHdF5g48H#H1!%anVLe)q-IgGsX5dFDxWH(R#CgDJ=Dk4Ug{HSAGM!4KpmtGQJ+#r zsjsPTs58`A>KyeWb)C9F-K1_)_h^D9X+R6Kmaa%wqASx?=&E!zx;kBhu1VLT18FB6 zO~=r+=~%ik-GpvKx24eTTkF z-=lwJNCq$zqhlmSW)#N6*qCxmd8PtWk*UPgVgi{srYX~mY0ktmEtr-}E2cHmhH1+r zFx{CxOfr+gWH6ab7Bh&+V}>!18O2OuUSkTFCCpN08MB;O!K`EonN`ecW(~7}*~08% zK4d;(4loCqL(G@VSIpPUH_Ug;8Rjf=j=9QQV`-LQSysbxEYAw8mK9kYE3pRF&X#8@ zuoc-rHi!*oL)dUOnr+B7V&mBsY#%n6O<`SZD%+Rs$ELCU*#T@io5c=chqA-i;p`}O zG&_cUoqdCy%1&cvu(R34Y(87SE@79lYuL5yUiK4qAG@DDz#e1|v7fS^v4`0s?C0zW z_8fbjy};gJZ?d=8pV&L>ZyKPXG+K?Qsidi_siLW>sivu}siCQ~t68VnqTnIXhFlXaj*I77aBaA@TmsjT>&kWGdU3tEK3p=_m+Qx+a|5|S zTsAkD8^Vp?UgqB7W^iwFGr3vZY;F!Wmz&4U=N54J+)8c(w~^b#ZRd7y?{lAU`?&qw z0q!&I822rAg8P}f&HcjN;qG$xxL>*7xcl7i+ykEEIbPxQynzqk9ef$SEMJ|k!Pn$# z@gaOTUze}Pcj7zqUHGniH@-XHgYU`r;(POnd@|pkAHb*cxqKc!m{)nokK#x3HJE}9?Cclth#BbrZ^4s|B{0{zo{sVp|zl;Bn|A^nqf69NxALhT}zvjQ;&+upY zbNr9|b^Zo_lfTX1<9`)M0SEztLntMb7Rm@^g>pi9p@L9Rs3cSqf`t&lDTE5Og*rlA zp@|SDG!@zj?S&3Pg3v|iAtVba!pp));T7RkK^34dN*FDS5ylGRgh|3Q;Z0$VFjtr- zEEARsD}a_-~QESyYv=z0Lv~{)hwDq+Ov<(XXwv$TV>+1kO{;o4E!(b~D%dD{8f1=@w$McT#Md~JbtiFT=WrFOk`gLbQS zn|8Z)w|0;AW9?z>5$#d!G3}SyZ?$K&=d|~=ziS_8|Iq#^5+W%AkrHW<5qVJ&^`b!x z5FKJEv65I>tRjYpPBByr6QjgBVq>w1*i-B!_7)Sxm&7EokC-f`h%PZz>@N-yv&G@! z2=Qfcj5t<&L!2s36Wa=%DU>h5M8t` zMps)Gt81)lqHCjTt81rguj{1irc2WG(GAy)(7mi1se48Ds!r8G-6-8?-5A{j-5a`@ zx>>r}x&qx2-BR5$U7>ERZnN$^-A>&u-PgKrbl>Vu=uYas)1A_t)_t!#qdTknLHDEX z7u_A*UEQA&A(4_+5+$7^Ne0Oxm66IyQBt%NBh{AbNOh%pQhlj`)KF?9#YwHC)>0>_ zv(!aOlwOkhO9P~IX`nPn%9BP)uSjo8Go@M5Y-x@(SDGiymljA1rA5*bX_d5ES|hzH zZI<4XK9oL^c1wGtebOQ6bLk7|OX-w!TDmLUlYW(clkQ8uOAn+!q(5asCS^tzWs_`{ zEpi#TtXxj6A=i{^$$_#{j*uhe`f>xgi`-T2CU=*6$UWs=a&I|Neo0P}U2?jdE9c3B zWmSgqD0#FzUVcrUF25zuljqCt%A4i)O=Km`fz=OK2jg0kJiWN>*<^5Tj^Wt+vq#%yXcejee}ut6n#H^ zx;|H*ryrqzRj=yj=;!L^>F4Vg=oji2=@;ws^#%GR`lb4n`ZfB4`a}9p^`Gev>yPM< z>W}G<>p$0jq5o3Nup3Gn${Q*h0u4ciU_+E4+7M%?ZKz{tXlQPTH?%OcG_*H#GITa{H}o_l8eE1{ zLtn!H!yrSR;Z=ibfQC_q35LmrHw-fjvkmhN>kR7+8w?u_n+)$5-ZgACY&GmK>@<92 z_}H+|aL{nhaNcmi@Ppx^;gaF9;fmp^;hNz`!*#*%UC>RxEfYD(rWh`whW2|PZZVWU!jWNdB#yZBf#&*W`#ty~=V@G2rV`pO*V^?E_ zG1Hi39AwNk<`{F0dB(xUA;$5>3C4-WNygWVlZ{i1vyF3%bB*(i^Nov*`NkE-mBuy3 zwZ=`xoyJ|p4~-ugcN_N`4;T*`4;habzchYjJYhU(ykxv=ykfj+yk`8-__Oi0@t*Nl z6K@hsT9atfnIw~JDs3ubDr+idDsQS_s%olcs%{E2#h7ZF8kw4znwwgg+L_v$I+?ne zx|@2K5=}3el1zO}8Kz8AmT8bF+mvI7wb9 z=||ID(>>F#rr%8WO~0F2v&PJsOPNcX%b3fW%bClYE0`;qE14^stC*{rYntnr>zeDC z>zf;x8=4!LW6iD1t<7!BZOw`1O!Hv#5c4bMQRdO+@#YEU$>yo%x6Culv(59(3(N)P z<>nRUmF7b8N9Nt;J?4+id(EGi_nG&b510>{51EgczcQaTe{VixzG}W^{?UBh{FC{P z`497-7LA3o@D`oLVkvE@XsK$cW~puov^XshmPku&OC3vsrK6>jrL(1rrK_czrMsnv zrKhErCCSpyl4TiW$+ir)jIfNgjI)flOt4J0Ots9i%(l$26j%x^8!YcyHe0q?wp(^u zc3VEN?6VxQd}_I1`N4A0a>;Vpa>a7ha?SFi<+|mDi8fnyq$g zd20o04QownEo+3et~J)$)Y{D2(%Q<}&f3x1$=cQ0&D!0XZ0&CyWX-be zxAlDeGzLIqP}rCF?coP3tY|FV;IY$tK$ro8D%y8Eqz;*=Dg>Z8lpeTSZ$< zTP<6lEy5OQYiMg^i?ubjHM6y}CD=OJI@x;LT($wWOk0*M*Oq4+W*cdH#Wu<|+BVlV z&oGq$s~ ztG1tP_iVr09@t4cW9RLHU9!t|#jdwI?4|6b?Pcs`?G^1+?7{X(dp&z2d#t^wy@kDv zy@Nf$-o@V4-p$_Ko@F0o&$j2-bM1Nd!S*5cq4r_+QTDO+arTM!N%krB*X`5nZ`xWEIGk^#n1DF6Yz!YE(umqF|C>u~cphiH=fLZ||0pS641L_6T z4`>$9CZJ}(h=pT?Wp6Z?`YtN zbu@M~aWr?tJ6b#1INCbeIXXHzIXXMKIC?k|9sL}cjy%UO$4CcsjCIU+EO0DzEOIP% z~aGL$M+s&uI`rK**xUaCf^pi-fwB0E*e$V*Q@PUr}UkO_q_5|%D) z5`qS}hUIie|9T@Iohv1$C*|bi6MDj+0+q@qOoUmbA+t(7nW&iJo5Xd=%pQ=Fm6Ysi zl9`-0(3O#!s8ouL3TxCTE-EY#y_yDwhlj=n#>6xZ3v3+IC?qO6GAt}Mw2`mo0HQh( zy_RqgrHIl*8KNvvjwnx5ASx1-h{{A2qAF2MWmHzxsGQ2Hf~r+TRi{d-tSYKrHLN9S z5H*QfL?97F1QQ{IlL#fk(BBb6B;izzY7NNrflP|}Ib`}mrk|Pynf{O&?f!HIWZqWO z)VM^&*1?sGS{|2@mYbRFyN*sBTID1v0Zm+~NqOns@7%GToTi!Co-a!`O3p>mqITJt zI82n_%8gG!r%6jqb7iAIQBTje3CaCj16@ce9r7}oxN>sSGLmvptjQfj%0-9eWxG13 z<)rmVOHa!khJsAK`n#lMq+|}kP4N8IC_6hTqc1vDMs5>VpS-^D8L63xiZL!DsZYAA zW4~-yQcAO=^mJD?I*A_D)i^UF*EKZPb7|-_CQox7ZAGGD{p&TfNgC)vtw$|MaHXU6 zxl)Q+64ELuC%19tz^rr^`nVkqn&sr8_INHaHYo?i#;`=tg>-ObWo9E?FvX&V4amw& z%gD`fe}{r)DaYJ8*PKWwB;tt{L`$L-(VA#Ov?baR?THSmNwunW)uEPF%c|woifU!G zs#^Ux(Fy5kXQB(ymFR{wwGXPU2l_YLm5u~D*rgJxMKvcXFQulXyJ`mq2Y1Rr4GbQT zjoO%&l#!j6Gb|(6C)}LicIipO`etY5Wu)W;$2Vz{ImGkc5%my$p$`Wp^+i478qzv5 z#g*>y95j-1HQ8 zGi`}V%75L=U@Y*U^em)(!L5?=GLrixBxk!^89_Nni-+~eOv+AqxaKr>%>ju@&l0V< zbyC{j)tKR~u~eec>A$Zr0Rif2f)uhZQXo%1JbZCE?yAZpDhdC6RgJQ;{=TB2?uyDK zD*gZaii$K0qne$G+5L;w`CsTrcl{L-m4VN={^B+T4NdFwu&hV9n@}lH$^73n!42NS zjTrB4M3qFP_kZ7r1XuRpv}9LKaAQPrc9Qo`f7wrV*I6x5>HOc<=^j+jD3qOgV2CE^e zQw?1~%p>L#3y6ipB4RO-PZX$OYAbcIx*CFL2ofPNGy5hgQI8ajM`k5E6&d{4ytMQbSN30zO(+_D5|yft9Ql_@+TrCG z&u58B?I)H*)aT^ow8_k9ipKVYVaVtVOvud3PIfg$*N~`$JaX_qHLz=ZY@!nQ$RU4i znWw&otMJOeeN>pw;OAvwBllMUJW?i6sr96qaQizT@_ly6*V$`{^+fbyVx1bknAo63 zs&NnN{${rxM*J%q($SUNFC#5EDLpY4S-E1H_t0Nf$ zwi7#u_lXaPoy0EULp4f`R%6uKY8|z%T2HONg4j*$AwDMd5}y$Ji2cL?wSn45ja3_~ zP1L4p3$-QIJ!_8Fvxr9>S?n|A^vH2{D*hBXEJ}(iHz_T>cpyTKT2{6z)s>y?O7Zkf zE2KeP(o%B!^~y<3PfJeA=o^@X{0(%h=S+cE#k?QM$dS!$ikwvJrJzE+GSNEUU}WFj z-+Nq=FNiM}sSP~_{%hi!o^29Z=jCGm3i);g#Mi{P#7oG)qdz>wPr8dIDy8FN;~RT% zY3n^}akDEnOy5P||9p}>L!2d|7cElbJQZCaen5U)QB_USkPDTWk(52GfVe*|;+ATY98Vi>61Ntq@t&H#C4SDw*N~_< znsn~yc4j+f4sd0(NlWjXl%D5mos>PmmF=tI4sqAhOy38;5q}cVOOU(Pf_Omufm}4J z+FEV9gd|9k1SF-lSNp4Z>T8cEE6KT)wN0Wj&O*%XtXnd27 znb>!4nUOgpqj6Hk;G~>(nK@}#&sryCq4|Qx)klA#p>1#)8r2gOyU+b^?Hbs}mHiN= z2w;4&M(M~9pvjD@xWXQd(QLq#-446^s1zPnT`3(ivs$?ZyVAYqX@b#i;=&d;HYwXZ z^+|WRb(IVz67tCq(y8`T6OqSHhNI|n1Q|(2kKJvbIu4;TUY(#${QnM}MYul~ zlliKujwUvl)S1Pd@{L0Vj@B#BO{|iBAroauyRI(Mh8WPMMlMhhTzaz z3>vO{z4D#Arm4854v(n`(T6HRy&n=45*{5E6%!R2iaS5_FV*}*Eoa=dyoqb+_;j^6 zqhtO>Jr~^dyj3i>7-x_Zi6_b#6^`^a5-W2w5>R-w6G;ty5fc*bu?By=pnsCvWp_<) z`)Y~|i$Ho85)%^QjEo75hzWIL6NWyE2oH%y(nC;$c|;c(joK9HL?4EP#l%EK#UKk3 z;S3E5jfy~hgfuoHG&~}vxQBjp*EGvlQ$$QyP>eGqG$cGK!Wk779^p0>kF6;JDSl{l zNO&0P_wbm|Xl%G1S5w6XCkrYa5RPy0VfPdHvj;nmA-ID%xvS1ot0NCGIVlUrBV+S& zb2BsIn>>miQpBLh$na=HSwtAtO5C`J=pbiEWJpwGbWEr-DkLh#i{Ar$+CNle`yMiC z-X;J5Xym*CO3hyk7ww_R`CfP6XTl1cWm1sF(Iy1T;q=MZQCTajO&)Dl=Cal zIX)piwpCmQRLWDt{~U2$+cj#FP;?Hj2@UlOz@eeYo<>B5VE+Yq3BUnLF9xO5LUonr z@3NruBK$jwMS=>TV(+5)gFA9i04h8bI{=kI7!kb&Q~^~%HBcSY05w4^5D0=mFbDxo z5UQ?G*Qp!SP3pVqd+JtoyZXMmQ~glgt$w@)go6kW38FwWhyk@h9Z(n41NA`z&`|wE zy$nePlI0;;50c#=nFGn`kX!@F1CTrq0S!T!NBcI;-M4EK^;C_afkSZsB5)uIfVkte zjXF2Q(O7qIJg8CU#vWB{*Do_S6ZuzJdASYJQT#g(1!e1bX4J*g^H$z3f)a3iwu*P| zht;QEFpB1(nW6ivIl-O-ld`gcv7d%~)tq4DiMrA=vry;_MTB!xGqVTg1gD`|GhA&G zke+xxdZ<)Fla|E=kUBf|rc%B8yk(=v0MQ9%(^ax*j2ll!5-c}~6L%z;7fFN0EYgNi;6a(AJ3>FJ@+ z4`|^AXg!)u`}j#o%fWG)fql}4p+%3J+@$0IiEh0?xVYB@TsipG<_@mZddSOjMJA<& zp%&s|ZY6D=l+&+XDDpExL&K2^5`(NsXkw{{P*$+0U(YJN_jCVIi*2XlKgbAP|nG1)-s6B83J|=U)&A zL&Ii7G8$VR9vlX`8}q@xYYdv(xMHGGUH*+hGdMK$K~qCC`G|@Ri3#-^snF~rIVSnh zK|I^tm|g#_F=0{8uuwED@^1{bD9%tc;|Yz7iAFJ=kiU*p&X5!|ZHaL`BAFrX#(b2h zr&st54i9;U?lpE4sm#qx&P@MbyM+m91M`qun3??#-eFO>r*IM}OcnLmq!oql)^p*z5Lh)BkB7`G3((c{$m^ z>1lnua`+z`m7)v$XP+E7?SVzb{|oQ@W%uy4=UJ$3-u{0zy2LF#7(Xhyf zhl_YzP-kRnIC8=swldJ&n7#k5G0u=ap=gc&ZyJMK^r+~V2o&PNVZ*M;~##Xn!5^^DKazZcX?I4%=V90Twy;RktEmeGU$Hrtfq2cU{A0hy3-XQe$}oyEBr0VeTP8kZFuq|g-Gx@N{Lg~!E6dEwjT?%>3wYv% z%2f0}OESLQ$Grd>l$n}}mf0V7rksaL;q|4zyQD^Wx&1P;QAPe&j6*CiUGWzEB>w(4Nj$XKO6wh}fUGjJFjQO~I7)bqH}Rs1>%1%SMF33?ieZ*D>T5d4IK9W#eDNz1_p z7Ow@O2%_H+IcS-+Pf~W!5Pbi_G4Lgd@qGopMvMJHd3k9meTJd?GukT3h<>wL^m@C= ztD8?u&(E%P1)gRQ0`6%|e3yJuIda_e0(wv;Xh?@5u_9M85vAzy& zfSceJ_zC2UK{;rwDk-oj8G%hH>MvffDdS_o_Vz3lK$S)cK{=>W>Rt6-K2?S) ztNyC~USe2Nm8hz?g31W%-w@VRHH7v3$6&1o!M-*HQK5)lDwqnPoazJh5B1LiDvSyT zM<7W+l6>y`QgsNKs_V~R-S<*j*MGNh(uUHNcjuIQYYO6*YJiG1^e>w7McmX~VrxxxMUYZ$sJ2u)sy)?# zN}xJYov6-K7f7;@)IgGhBo9del3GZLkkmm^f}~tXb@Sj#_44AH1ZG20@$d>sV-c`r zUjXagHc+y_W@2EoAgT8en}dj@a)}`Pf}{a$Xuxl1QQ5PBgBnha#7K>xUWTLzlIDEs z73x(;S|DjJIc~>L<8dWp!3U7EVsIyrWg%&M9J%BLU&~&n-o)g-K~1HmK{5c64oH?N zpr%uAVR%bJvdr^`cP=7wo zvhU=`JDnr{Ncl)T>%0B}YMF=GrI^`rnAr(x3KrX6y%^sStEu-ev}>rf)H-TCwSn45 zZKB?x-laA}vH~P4Lb4JhD?_phB&$NQ8YHVjvIZn;7E)Wh(C+X;y9+~G%Li?65wvw) zK>G}!{R~5U7?OcLXpdoNkAq$K1<4@wg!@gcuF$@&{+9X<6Lo?*3CR#hI`gSh)M-eD zLUd0Wa!CKtQGTMM?W^QGbqN!Af%<{E2+1%=hC?!Bs6}{tMw9ngA>Aq!J6c$!nc=5yew+0py_$&20Ch!C5 zzWOaBV=#e#U;=CV5vT`@S7sA=wU+?IGErfNnvz#ONkKvg30{m+nBwbb>#RL8&PhKJ66O z>GewHnU%BNJcH1sJENjq{ELq1F!)rzMvV@wJ!ur)(&PNQ2>3+aJ$2AxS~(Szu0I)~1s^XS2l><-Bukn9P`UXbh!$wWxL z1j!^w_JL$_AwAT~?aN+nRm^RQkK5EDZU?>K_8D;dI_CBbNVv{y? z27d@mYkv-R)NkE-UAi(8|#@vHU?{=S&kE%bH| zuiG%MS(v^jPEhF&={+7?cVk?$|M`Fe^cR@cgY+T#Q~ERdFnxqRN*|+-(`ZJN3&}i4 zB5a31awsH+L2@`GM?ezIiAEOEUwUc%mOeqBB%JgqOzSH?T1P_?>C#jjgPQgN*Jpt1 zHH_rHHCZ_z)Y*KJ6ukc94cbo;s$@6*3SattKL=F@-B ze?oE`Bqx^`xeU#4h+KwYSVjZM@sOMV$%zFF&j^^@Noe=Mb0?S46Eb7)$8lxjfQXm- z#xGc%eB{?jUv>W+k;|A-QHy`kwtF0l1M4=wH?iyt<*h$g4=N&;u`{KRkeL8Hx1WOM z_DpF!w}1UHb9)29)UQynVL8u^7CpG*?I)%(Q_amXQx&uPCSsW>js5J`{rqg-5kX80 zf|&_sLKr6#%7iiDOav3jL^08jd<&8@Ao(^VXF_roBoV7~Ac@BAd61l6$kg`0%+&Xo zUM3d9yub(Zq9W6~>IKUs&oa{vvCJS>bfK4JrsI=LFVlnRjdiOhgS@E4kj&3#5*g%2 z6+m)j$pP$Q(lEfOOkbuSB$q&PDI}K_F#VYU7~tiQT=Dz?&PG(`_)}R>{%-yH?OJqP zzti4r>A=iyFu;RR(INgt%YDAzQmR(->F?}VFkyziP19n4hch?=$c(@M7d|Pf?t^bM zGtPtSSd8oHe>Al#OIO#5I5 z^UF#ZGkyvcJ?&p~Zh0~6(zMuVD<wOdGj;p6wKB5Qm01->PZFN;@iSp}l~gr55{*e5REvH@&qq!_G& zEd|N1A^A-{TZTnwq50VNC1!2eN-XmA3fRhQ6}BoQPeAe{B)=9K8!q-SY&P4 z7`85^?*q0Dp6H(WN8`!HvT<%)*(Mm*bN_rmOSUtjm2JhgX4|lB*>-Guwga2Mc4Rw2 z@&Y8$NiIV25+pA}@(LucLh>3U(e&ziA=|}cZP^}dPqr7~WD_y1H+;0-hJb*8MAjC7 z7q~tHTnA!YGaz}>hwC6@ZP{!#2fa{u;ua)-a=-tK;>l48ulQ2ou_M@5Ft{(XBO&<< zB=6+2ud--bj7H}NB?sIuV2WHYz&Pzi8KM&cQJ`tuN2xD(c!lxSmrC?i_Z$2i$oW z+&>V9>_Tid|MWAPdgoge8&>kzT_x0OcG+d@N)N{?5XXRe!tuT%*0I|#$m`h+>_&DI z`wsgqyP18D-NJ5#fPsL8Km!2>0S|!yfffQ00v!ZWA-mlR@=nhrm)(s(2C^4qp!bG zE12Gk>?QUx1ZD^<5LgS?tL!yIFR(#ifBy9TjF7zTkL0rK8S92Bt%F`~b^L=P<3?7% z^xj29@A(&fv(MFUZp~^mW^&aPtK+y8LB;gmXaB%L{vFL4fa579xf)udapS6CF|MWm z(d=q;8XKZjBWYxfqS0#%8l%RfF>5RuD+FaBCS*dhPz!>< zd`*2#0|p9@1gx6Q{zbp37TLIz&{$oZ)w$`|35I<|v}(F(dSW4W z$FxQ~4Xu4NE)T6KnAWI&rgeZOgNV-8q!Vo+h*8VuYce%i5Y&O7!y}0YG6Q8S5fLV)x( z9)cDSw1l7)1Z^NdB4~$38&85K4O$&trg=j%RWnWVre?b4EePsCfHb@j1dSnxgP>Va zMjFjboP`8}`Zx`VJ5NwpbYu`p2o#D^1)&WmXj37|^no^>1fi|jkr(7V|V! z6{jT9Ec8?r`}nHDogp}V$zLw&UshG%sj7*uDwO^uC_EeqE*xip2}d&f+p6%+NKeL^ zNVJa{i5#`cPk;|y)pAc&P5(_*D0NFvR9Hx4R9KiZGAcR(<)(R3RjWKzHOEz9t%wed z2?|BqIm1x;A++r$H2iP1!tbJR_Nd2bMa2f+*4vx5t333&%9{0>4Zf66miSmtqB@+| zu9zVA#uN=s?xfiaL2Hy=CaLX!z~SkE>B-JV)76AVMWGZ*D5p{s+BF}J64u~Gprknw zXg4a_n;H|2Qt_bNZk`kFz$bhkg7)ZyX%Vdh+hqqf9njI&tPeE@pEd`Z<`7v*^O@$b z=7{E~=9uO-av_Z-j#AMp3N?0gq>UUT88WPKTn<_ZKo9uZBEta~8E zK+ST-__DZZ(7LYg0AF3VIUNzbPO)izAv$R867iZ}HNT;R-|aLHG=FIR zUZI3s7`%$$X@ayHJ+1#k|o6jz!n!3BASs8@kdl8xg0^bhSW=LUG{ zAMvlAxAC+J?B5w#o@z%XDyh$;Ga5bN*$rs2rh6kp^F-InRu91P~Zh@6#t>-MClkj zac-{QNs%+IkXwUdG~6m~H3U;3n3m72<<>#)CImBZ46mH{oa=W+>A$S{tnGnn>;C6F+aJUVr)STxYZsQIT zGIz*7hSreid@IBhduCMj(9>&{+h?M18+RBLJ>p;V`q&@f=?!hKEI89B^va5wX+_~S z?l||QJ9@@_fum>ho**=z#7=S-F|yxrr?}JH_uLuoEO(AO&t2fq@e3eW2m#WI#Sr8} zPyoRa2$n*y41(o_+$Arv*SyHy#PVI?L$a)#>lSC=dHXA0yHwL_hZ%_bKy(5Q{yWq zy~@a!<0~P2`SN@P9!>H$La+&fcMAB*;0TXm0Pm{jA=r$YTjI$#o;vZzF+Pxx`5=EL zD^Uqkm(PkDvb^msyJbuKX9!;&-F?D8=I)bzi(1^UzS_)I*#E-p3u%jL72(TA@X>C3 z`6!Iau)C0N;o+BW!?)$z5l+4X=68>e-+d5#4gtEs3xY3S09^6_^DiNQc@!1=*avV5 z0+@I4srUuKUI;$%yrX+W$|z=Ee-GrdFvuBvCItH-IFQc|;!)`9AOuHCY}JY%!jHfl z4&{gOD3o>xf=?m%tbl)+ABoNAVTdy3Jnw8d{1^n+Sbq*fvcxarJ2#v2e*cwMr#5=^ zP0Zm0RCJ<$QDNt#MzJ^B*cR1XwRdEfM75a1$^09hf$ViWkR3y2l%IxukK=yhn4YOs zv0*ju9U{QjNwfI59**Z=j=y@s@xCJ#^Q$q&`FsJtgkQ=p!1i|He{&W5d2vCgu z$C5M0-}2viwD=^};;Y!2AjLuE_?o{OTYLGMcAme4*}uU5z$4|k4#5owZWbU5e1*S? z4e%`pe!^}1hjdm?ZBhSoYkrKsg@FFaAL#Kr8Z0cNT6P}Swav?Ld+kEZ{x7KLV^Rg2 z5cbp>J)!0LdEj)HVZ!eb=mu*K#6{2O;(y~Gcp&~AL;TB=#OIZrKnW6}SfB+)UQou{GP#sY$)DUW-S0JP)NYU4*?6 zL_kUdDK1}#5~3l+LrN?$j)i(cLpR4l1H>^UAZsGTB5Oox{V~?A@U^U&&=O%RG#BE9 z7Ld|GN`jPJAhZ%%BNI+3kkUVQ6E1WlWTBHk#_W}aIdfibdSb$zMn4x!J@5;{Sm=t1 zcJnV7=Arn$&NLe6dg_I3ac1Q(4$^ofTkSYzSGKIn*FT8ood|?RT6oz4V%lhE0 zfcG|0L73y<7xX>@ddFjWCqSy4kKWf1y>u9xLx%|{a@fLsw>5&GJfhd)>+R{n+nC(9 zgc*>k2&qc>!c1Wnq$)$IW{LUH!hB&dhH!zfP(W+OXn?5-scHp6zJTm0q^d)z#`A}; z5RtXYpDf9xG<~^G?1iZxZ@TzShxKygyvAK;!exDX#x0!0}r+#|&M{m=#8m$(guI046R)AC- zq?$siS%FprN3_WDHbW@q227~6ZxOO*Y{`*V&U)U`HL z)b3w2F0j^$#AUGu!e>QuB$dvNi-ktF5A~s;#E2 zuC1Z1sja0A)COsTA=MgEZ6MVaQtcqs9#S13l>n)Zkm>}f&V^ccc4)$>jqpILjls}% z@j=_I2wK+*XiFYiZ8HR|wmGD_dZE>}d=j+U4%$vgGqed>)P3C{)gxcqS=$9tJt37? zV$f=PYTfSM2io2k+Fn@wP;3oB+uI*nOTJG7Q?>muw0*Vxw8%Sq2~tUr>QkT{piRg2 zIT=zZ&)q(2a}bHS{v_U77Hp`yp~az9*FP#Xx9k)iLpuZ&9qM0n*?}4T(nFf*H^tp| z{bpZywiwzGTD;v1oYRiP(55~~YCe&T(c)C@+OgVk+VR>6+KJjp+Sjy`wNtdOL#iL7 zkQnY2Sg=U`P$g*KXFn2dSZu z8c}lK?$GYUm7x6YkQ#pG0>SE%UM{zZRS`hD9O&Ndsz zXXsyDRH|O@Vr)-nPkFHY4r4nC`Nvw6uN_P6Z}Q1%&uedBYAe)DsY#G}4N{XKHKkB{(@X7bFSYkDwXgfAeY1$#1uv*A zd1^%#Q7fXr(i>iCMFCMOYDE#hAT<@Dgv#!B#I2&Zynu*C5ii7wCeaM3>5zIWU$lxg zNX>xMtP&$vEG?G9m6XBczKzHg%VTn9K89R9Z}hdSs#p`DD^?S$i!~sHRA~;R<`#&x z#6XPhJV?!d?&yl>ne$?VKaZ|DRSNqJXce-u$)KrSI^8Zn=!(&(XpDc+{9Eg49Mw1b zVuCimFmn|DUJ<%tU9o{1U9mn!cOgPoERWHh`FH4wabi1!uGmy;CN>x2#TH^qv6a|b zY$GD)crm2%AyojWC6HPQsb!EtBm4?Tt%Ouzq1fJouGmTJETV(}VmFNLDj&LQA+;4! z+i+NF`wMoT0lR%MyT~P4?PGTUW;dM<$1g~&!AWJ^?}%NWM=IusgE3UOVjiTB_qsk` z93movy8%+0N{-x-qKYed1(UlGk;|V(upK8(!q|=%Cy2;CAb;RpNNp|< zUlS)|Y>`v2<@sYf9kKY9Ka1Lt0YQ83HC(mjo_@!y6a(5xd`z5)iq7&cTF~j$!fV^& z?rf{M>2AI`YdX%l<6RX}X8S}jUtHuNcp)Zu2O^k1jR=0sqMGlBWg^O)9<1gYJS+5@SNA+;A$pA?E4y##Oe7+-N4CU~EZ z-~&a*_lp+*KLddGVSx8TYQGQQLrhaUx{Bs>QhLe+XhM~ zk-PYvcm{)bN<1xo52?=}N;Xd&DG<+sBjR~T9mQd$W4N}bevZ*UF5~{E0jp!;6-3un zf4aI}+e|3BIALn+*e3M&nJqAg*HO_M{zbcV-csS{A1zv_+ZT^4S1z?)vHko>`~}nZ zfp{Cw^FK#3R&J|%P22yrP#=ExMVv4IoD&~lV848dLv*xGM#SnE9jnvmI32GObXuLL z)9EBgp)UOfQmFD15T)#=zJt^$h!XZws5{OS>J$&LI+G_drL$pT&-#c(%jcfEF{qm_ zh%I?ybyX0tx~h;m=OtEG<4MHooVswN8M;s%S|7LosUPxn5jwO&a1l~hO3c{m>gej@ zO6p=`dkGm^T?5_2B{y>s*$dxt`&wH z&D(C^HvS{NPdyO)$DHd>)_2`wvc8|sH5#ru<3rx>Cbl?JW$^&SxejH0*F7fd`-N%) zzaPA|;kc<^&H1=l{gZSN=eq8?UT$c0Ju$Sm5VX1m?s(p1KdJddmaIdG019;}I+re0 z*H_n1m!|8l8=yNiN;ht%(dx=b&$IUX~s8-k&I z;DZ+B8TLe`XyFC4&j8x77}{|VJ&?n53zcr-6QP}|n~r&!rh6081flQ$o zD={;xo1Yd^LD~#yOQG&lFR@3x#D0N@rLA6KX?qc|m0uA1 z42V64iA5CHe8gTv#Pa(^Eq>uU9#Kfj{a*BNGTn9EEv!&CbT=U#0BJ|Q?kC;PkS+!3 zvL$C~@9FMiqw#?bJyH}c=5|GeCehMT?q9q2>|R+0%>QvC7NyuP@6t@#aS7QDQ=>YDRwsv*)PBPwe0FFJPmV(Qu# zjneZ^1aG?5?ECITq)S#Q0L$#0WXIT6d4kM*5-TUwMAS;FSV1ztn_uEl3AKItZc%>(C*Pb{0ytJk&}d9%`jqj*$RUuU+7ek&-3EcO6LAg>=0FDOKu= zsjUy`2G5_`41{8)KZh8YhjHCP)(@jg+DZq~jpn z6w=Kg-5k>KkZu9#mXK~$D81&T_6=sfG>vdd(=oNJeblzccQ>MEGo?Yg*9&Z)0k(@V zw)v25Iue>9TYM(l0?e z3DSKaoeXI-5V|0p3hBO(?g#0#Lg|{9+?&!ZyfsL=jmhorBR3t=L)}}0=wUCwEqQQd zv>|t~OhI~p7hD-F<}N|cI=YQt5I^c@l;^$3OO|!Af|Ns+WHj#?2s6{x4s3TE{CF`Vg5zC?_GWOP`l=$ zxO$NTtAMf?gi_n zBxghVHAqh@F{>*Nkw;(@hswj`;gFsT=_!zYy+D3h#+!5LHy}Or`J;$7=gN=SoEyAx zU#qCoagpyXp2OVQYFUp_M4NNv$864RcW%2%9CZ!=3E(X z&ZY4)#(4Dnarl@d13m(0$g@1m&cw{V^%O@ekXK@67s`v|#d5w}ATN=Z%FE>C@(M`5 z4e6PXo&{+H;~a>dB}vbNG>Q@~fb_ybxzNk(S}(I3F|&(&%;py{yXFP6&w$xonAr~@ zz1YX>9>nYuBUAE0`7_MyA^B5C7eIPRzI<3l94&?P@{+T;U&vqMO1{K4cNty;dVp+C8P@rG?0 z&nL0p6%A(gf&7R3r$Q*C0u)N26-Hqpy&lpVAiWXNXb$uaq~C?~W=OvW=`E1nTBvXy zW))GFtg~+DEtHI9gzOS<0rp(G+A89Gpm$A%qob6?OtY;3fRsLL0fOp3vHZq z@4UtDMRr!HuGB)xq0~@NnDKo`e~_;PDk#vn6Ve}+7_&;45{WhN0|n(Rhx9J2ferBN z?r&EhmAXnp#H>2c*_Skr*y>d z;s=xcljwRVsR&-Br_xL5t)Ps8N|Mq?Nmf!6G^ITV=|hl4!}Dj5J`8Cj%A=4z2I=FF z{=87>>xFlK=Z*^{6T|C!Fd6-o?~V)l+zW&I4B#D!;YITo--F2%h~PzosQU+vnx3EP z-Z5I4puC2`ov2KL^w*I7CSRGXAomsV^TP!@wD3UUk1;8owVxQ+k$if&2Wqj5}GiHI!p zC$fXu#cW;Ma6;km4_iL?vaFK;?!}<2IU~K zH>Xw7u*|&NxS_eOjGVO0j3+J?`6{`kpqu*&l%JHJmD`ZM1L?bvzE_~!K?vT1^sgAf z-<~&ue;}^@^yg~qyOf!_)MD_W|Hs~Wz(sMb4SyD#nbjC&dqYLBE6CEZM5&5hQ4||e zR#`=sQ0zvI8e^#Z+$N7H z+SJX|-PFSrW9n(@C1gp+1|dfXIa0_Sge(hL5xfjtHVV1pep7F~)tdV2t=2S{Rx5W3 zwOYAzxYf#z|F+t{zSWwB(rQh^gnV46)tXKSj*~kicUq=BPY@q`oxl5xd-!xXpU4?A=%eq_BB-2!_kjZ$h92M+twYlx#_UE>ynI>KewAWN* znq`_TWV4VhLbmQT%{9$qoGjafZ2!l{$tE9-cR}mMJA1-ezjPm-Gj+qd{xQ${?|Dya z=YEndYMu1NO}@+pACJAZvAlDa6#warlf#X6iAh@~jCj`+pwY@shR0E7YHP^t_Ozy@ zrsY!l9@8=*yZ4w@2sygNt0hb)1&@Q4J;hP;{*L3I-}D+(*cLh1~CeX}f8M z=`_<$(=O9))9I!&OlO+TGMz2t{z4ug7h8fpFe~+sJ!zjc=(xP+vQ(q-LQnRE_M)l3LE=k82}&sD?qg3w(8TRI^F+w$g{Q zZ=t48{=M7J|46C(wx8@sp^Ky1s$PMw66^TNa{^`cOMF#z;{#aneH~- zW4hOLpXq+n1EvQ}51Ae|Jz{#)^q7#-g`6SekwP9NFLQCM|_pl`m5f_d1*h*e) z^Wr8~)-Uu|O|LCk$eUSW+7UU=Us>m?2}(=(!JJZ$&M7Eb@T;^ozY0p7-CDG8g>u^b zs%@#8%Svfo7Ike~LoMr?%o~j2HPxl2q^2b&dA%vT8_PE+Dfoi0cD~VH8)bS`dhrs| zYo^1d*G+Gj-W2jsA=5_>7xD-ppU^O>ILekCs9q71#mKnQ7sKEM+P+NbXE{;I_>S^lb$niVlK{B;#EyJiQLRP(-^n1XO7j;Zxk znHC6CG7PU@80W954L6GqO&_&v7N43vYx%+FrZ4mlCWo*2#`JB=BtMvbZ27@2 zre9lpV3y2=mLGI5%Pl`Jnme}qptG6xL0x>Y){h0>9bk?!oAeJ*jqZ9a0(v-^&1?@R zvxY)RN!emNHWU!U>D*>d%lx~Wd*~kokDfNO$hgN$x5Vqd4w?I!`fX zgNtf|@r!^}ROhF?)s-x%o?RY_Wz(n9FHIA2fsm&Px#(~2a$_?eo}N{I{;cqhRda$l zi5DK4y=G$7bRicunv=~$s~JL`*^pbP_d12TbG5C?qNZ?qP?{snH4itBD30nDQl{Rq z9v!&%nolr~l;;1ZWR#avIPK_*!zRfz=V*dwnTe8&`{pq2G3PPvk>_eLGCE}Rx{^8{ z(IuO>SW->I%dhh-p<(pUzYg~idNm1))l59KH#d|9y(-tt*@qb$l@A*ZJocQs;0hnMb(gSg=&X1PjhRB z9K$P2{^V;kM&fBTbwmK)h@|ws!9P&Lh}4X}M2PUm{HlGJ=IvV>>^y>>Cz3q)*Q5mt z=tM`P#wUc8F~dAl_xtB?aMWI=8-1>MUg5-oiS?D0jpn)1{BY{|=2FSN$6O+0-yU@}}4uQsm{a+Q!z6mp$5QR$8dL^|!YBfjNzQ+>Qu zs;;)6tiq>vhQ(1SZ6z3AQd@^P8R=E!(1?kt+888jg??eM2kYC`buH6ty=_?F_2!c` z$D*~(++*G-XaDyd72N;Ma}jZOy7a z&Ah8Qv)w|j(catH)qJM;Ec4k#WL6v0w~%rEp(&|(NhxU=nQ^>(GdC_dAvH5DD?K?Y zE;%DNJuM?SGcPSWyVze{5vcOj`j^yKmed7mic4yiq$cYkoNGR>II8Q!f(i9?C8d=i zQ|>jNYd&9^KXYP%ZrX4yFEn2&rC(;g$b7MRkGavj*SybsiID4syi~}{K$Z&`YpoRW zs>{rmnJ+hAVcu^(V7}6Pm5^5p8N;0<5V8>RI<09BFJD?$&VaYf7e{#TPu9ew zC6)Oq0+qD8I9>2S?NAzX_`^_b0@S{L0{<~eNr(gO?UR`anYoGS3Etf1M8UeEC~r=- zH#sdOFD@-TIVCPRDI+T`BRe}WE;TbdJv%8qEiEO%+qR;(I4dbNH#0RmDK0xPJuNOd zF)J-DGdnRcEU^l+aGa``IU%sB(M3f`E~Of<~Pl6ncr^sNXXk+g7OP_hmcPb z@=hV|5;9GUhlFSOYjZ383;nc$;wUTemibps&^PhOx&?t6UPfm>^5gtk!cJL@ua5Lb zeKEsd!$3isv_c^yQ5W;h--JTFCC}3T@um$< z)Q48zMs&Nlp()7!U}mtm&-|nLC-cwdUxa*?keTYABjj`UnSV3?ZjmeoA)hDY^M!nY zkT2BYg`KFJSyty?>aSZd)mI&;shj4n)9mGJ^+h4Qb#QsP^;eUsLsQO#R#$1`=t`gV zbwT}-B@DIo09fE#sLfVu8E-M7p5aZ+OiWHJk6IM0mgTsJZhI}AEyr8B2>Bu*?-TOn zO|`U`EH){9uf=S!Sgb<6Sje=P#=V^jEe`W<+CeMwUMd-!3bqoaMcI>U{DB&(%~bG@ z6J{~$E$~&B)C5C))PO%ut(Q?)Qd#et5)*24mL3)sf?LUKh_>{x^yPiEGC}{+hTJkK zrcZwS>S`He8Lahl}#zq=bY+hKR@xvZ8jH*?XyOkfPotnuqh( zwBB`r>Y2R*rP_i`tywZ`yg1ku(XIN*gUx_M&lx*Ts|f_^8Vvi7>uj+`ck9!4@Q~@f zs~DaXFRWnqYqtC%gA(kzLrTA{n){N{Ra%3r3+WZg){YEeOg9Z zn#KWx$a_*y4Wx~Wrv^EGygZ=I^%~;_@gaRvl=+B?mFsqEOdQna=aNw}Np7jPG(bv} z(xiN8yfj5BkmgC{(qgGjS|x3gHcMNj?b6xOMbc%`_0o;f&C;#X?b4mn1JaYy^U`7I zUFkFFbLmUzYeQ#)$q;SmV;F20Vn{cPFpM=!FibTpG%V7qA7Rk{m6AgLhW;_o)n67I zWLV-YL*_OOF&OTY?B$~bX+8?j{tCNYq9uiC08K@2D2%LvN}mXVfGmP|{QCELPM%auZA+;O##uMzUKLcUJO*9-ZE z%O$U6bi_H9eEuD!bq`u!aHG~Q+@$?>jn+A6jnPJ{nc=T04=l^C&BNDNj`8`b<~4nq zSA!v0lH-FhCAAa$3u$e>@?bNn&8rD4nOMMto}rdLrs6xTp2bloXc+`*7S~oYoS9Zq z+Wfk_;xunsRtn1|ae3ZEMhO{->2c|K-jukM^h^c`jBK*9GFUHal7OqTN@`}5)E4?{ zwLVK*5-4C8Ua95FtcZlcNa<^*yeL<5RxA*dmek~jv(m$CR_LPt8J6h+j|a>rV{>-fLp>Pu)O|SD;`s-LHnc*+5tI)5^ud1yk>I7<QwD!+N?#n+t0`5>jI!(CZ@*4?_aVC_+C zcqR9Ue7lb3K5w=D1;MMSiEg*-rlYs)u$*SuX<@weh>#x@@?%1Ne4piX%Ndq4EvF0l z2_Ziz@JJvFDzmRxFjew{z|V_mq`c4DtI|@ z#5Yf$(N7N4;?{wxx%#41XhGzfD7SLF+2)FliS0Wej`hl6BQmo`=Z~LMP*^nkpla;c zd0KTzT}2VW&9{6E%gc?Oj%%K}6XA}0O?Z^ypw`E>C4+BybxH6Beo)FJ)}nIW?)vQ5 zD2XyGo%eR?WStp)+4VuY!`b;5^LXnA&)4MO_GmdoMI7wby~nW^|9AnH_B3&6FPdx{ zZyG+>yU(#mLN5KZT>2LV$JDh42M!wiCo1p@*T*Mt{gAe$h>ARzn3R0%)q3g$GE6l! zcItF6J!9yxU-$4Zt{ZOJ)9K&|BS#(kW%u07Wm%d5h8)bv%{%srM|a@DF`DA62gi;( zmJ3NgK}$c87T?yA*ulwDrXIWCk8~ryXj?o$W8cWI>OUq*QL5d{B-rHOMwb8dAcGR*_!9QY0ZP!2CQ$c}# zL|wG_s3uoT$K2j~$%Df>4_oL)NaLgmX^B)Lt(MkGr!tH?mx0{%(tXn74BwuUUXb2k z0Qa3iHgqvq3=V_K(B0rQj56dKCNgxZFf1`N77SE(LW+CVpPPWh?x;{BZ?!+BbG&MjMx!zdc@fg7erheaZkjf5l=-t z7x7}m#}Pk7{2FP9bVhcIOo$vAIVN&KNjt z`npP1M754upDk!%HWcgkTCTHPuPsX-WBTH#(&A}#qq7UWsVqb_mq7=anA-g35k*d0 zQSGZu3l&#fSWukgZB7@Ai3N;Isut#R2d9qra5IVs_a!%4Zqxe1n=Ch5Zm}>FdS1wf zg#3b#U)*Q8-ExQJP74E~mxTPPkY5w>;s3S%@IlMNS|9h2kYC#xJ9^WhW;s%+E`t+UGH1`Rn+uNsm4Egtu-7}y;ZIJb` z`q8o!>jSvlnwmfjnZ(7j)?JKi>gn(_#u3{3M$prOHQqNQ!5i}*QxRU8vzex8t~rO+ z-vzZA%)Ol%uZbp)WRrDq5|&l7xJl3pPIeJp*#9P&%)7sAl-hNxf=>S5?< z=xazZWEf5$9Az1D3=0g44O^MO}Zi`*-=D58E!*`jPaTCRFhHY>r6k z)J8wj`S{pTC+fP09kr<;_}j%slP7+}xbf3x&Mse4y<%NRRpBXlSN-p%pW%niEw5YN z(L&H0mNzYLS>6^h|9?lw?+W?7eT1L)Ebm*GmJxj37xD-E^0$YeV=(Q%EnjGs{9MQ% z?y-C+RGi%u4USy38C`9Z;V38BaO(E6&%gPh*L1fqgq_9F zvPgZW?UuyZ;@{8qH%Wsz7t&)&G74m07{+E#dEo4mc#ii^3jkk7)ILE4V zoXi>uP3=oH4Iq$`$$CtRsABo0Wn$?zBa5M^4u0rU_W+Q+<9% z-E)JJ&S^E;goEBR7_nGe*CuV@Jh?Te?eWU043sUdMJ>6ZWf5)(l6x>^9Sft{`fez4 z2E$sYiqrD30=yZcNEiUw##{ zxsuAt72$lcCr{Ux8ZC~CUy7y4*@Es9)2C02_b5lxFCEBzTk9Yp z|J-okMQgluh&4g#YV}$Zr5V;_NVTR})0y^#Z<2>^bwgWHD@k)CMCl+$D=?={+Y{*8Z;l zr`KSPbuKq1sX+^yG*_qP3Q41No>rS%mRo(+1=fW^=`57vh0;YRUH4i2)va=UhP&!&r55+$GRx^ zhwFW@qpC**JB6@nmReV^RBv5oT`m->P;Bhjv94mrj$+re(2wv|!&XybO%3rdyw(~N zs_DUx{2WacEmCIFsV!ug^~f}On{0^aw$Cc8>#Xao8-(Hzic=^qp|~3k6i20oRHgMQ z3s_eV-lJn9B8#L=MTc{aZ@x8D9F_KGE~HPYqA$VA5j)|}T&dkWDvnD2GneS0rEW#j z!a-Q?TdmvK8e-ihlx}+>&Y>eh+a9fFsnt5xP@kvGi1n#ZcnQ|J+j^#EuhXq(2&IQm zVjAhLx%Uye+0m52cwfn4W=FwJRNGlKwdtEsm#p2}Xo}Whp)Rm;Cc**hh1QF#7hCsO z8?Ae-`>dB(FSTA~y<8~0gc2*1-a_dkl)gggCzSp|86cE_LK!5K!3V7Si78iFud-fk zy~cX2^*ZbI)*Gxh3MEd9G2?|YK`0Z2GD#S-g)v_krwU_%FizJhPDe%0yP$-pih`|1 zU&|ku7LD<~m~gKj&fQlM%plYW)K$!zP_o>=1h>=27-2izZ+%3IXAf8(v_52gSSayA z86uPfp?LRMAGJP)8J-YIqEON_&1W~{&K&PsP^U|!4RV@CS)Rs<7= zOFL+NNh|Gn>mln4))$46B$Q;KqzEN-pEg!B^uT`^C-O9i?#03V5%kgAh4L*6`rXu$ zWx+ujRd%=hHnfYppp2ye^wHwW;N|QL2zQ@>8m_muND|ClTRjNN`i}KIty#S*6n?%h zlnl+iOiZ_%>Kq(b>9OeJh;EJ6PpqE`WvEa_HQnjA{@eN$o7SwKTfeY=DU@MC87`C& zd#ztHZv0j#CupPLk!+eH*XEJK9PO{>`)*PEsmVW~geMm2>dSpiCWsv+E^iTmf3+Ff zD9sijd2JnplF8jnC9C0Zvsq$CZEQF)16xO%nH#6JPPXH0oo&b4y4bqfqHJ_!IYP-5 z3gd#&LK!2Je4&gL$~bPEYX8%2nd<)w1`j{B3m%4`2NG>(LQ~(wX2h!6K&4M>mHJlY z6}kHAOK9M$-8^kG^|bKT+Lq}DwwHc#e$F($Ymxcj_7YF<)s>WUGqz=NZG_RbEO<%F zr0t4bP+wXby!GEQ{gC#uX56RU)6w=4w7jBS*~7iqk;!LGVwZS+758atqZoRcLy<{q zy3+!|Mj7;4TYm=YHp1@YCWo~Rwy|IMQd^uY-ZsRRVDs8&`%{E6RVW2QnI@F!LMgn| zmTXJ0rSdaf@(N{!P-Y5aH|-Z=mS*%80n?-hB>kRo$jTP|voXXndUif!el}PCOp6=d zq(QT_wl&UQRUW*>*Nw(mUs`}4h*m7<7 zYLQT831#+P+nC7bZTRaPVIaEg(webZQ%z=VNPy7%C@LI6`7#(b*?E$)g54SZvhXLm z+9pc{SJ|f6rrHXm!M5qPLfZ`6Ok0s{mTk6ej%}_`_&=TTjY2LJ%3+~=EY!|IO&02G zp$3F{p->+Z>Zc6@UbfA*mDoycWl~pLxy@%=U|VRbu=#C^Y>RD`wk0-hxVp+i`^a02 zj5Y6In6!XNk+y|E3tFN5pGS8lU2C`KESLtJLkWoXMGOg>9v6m2I_9N`z7-6rWHQG)>NI3-u{Vsn&@} zb3>D>_}XRu1$CiWjBP{2z1qa5yx}#HgzoW$zB8u;FK5TRc6r+$2A>G%XWMDpCD9%# zgyL_Y<*~+J=^Hv^$X~lrTx%nhY%ka6hM($v;h ztZ&-RwVh`>f0lL+7}c`0Ae2g>EG~|k@s|qT{A8dm?M1eWXCrHPa=+KM$JQv#Kk_45 z;ADb>uUwn_=T_Be_ui*VU8O>4hBVW5X>e=4LOc22(=$4(t}m_hm+5Mc)1S}@KZqCJ z8llIsuo2=X`xche*4OF};%K`u!vX7n?Pi{?wq0qv%67Hw8r!wD>ulHCZm`{GyGbZD zLa7x>o#5P7WvNh>359vp3ZbkN%BuagTl5~+c8C6Swe23Bu2xp-9j{PM3O`+~oT`tc z+|6a{vc>Bg1Q_G8Fwpu-BJBi!6~o$U0!^D=Yt_gOup2M<{8x`Q)8w<565NQyXsdND zJd1JUL;s|SZJkT-IfY=i5g!OXtpGV z)%FOUu2wd)X3^gHfHOkzHW<6?o$SZ4KY?Lfi=m7*l4!Jd)*1ngtFs^PBiGCc8>N(I?n5^giM2;;8?{;)Bm5+K1a2eeSo9u%BQbX&+_Jv}f6~?K$>b zJGwqsDCY^~e4$(*lnaG&kx(uc${wLK3T5wp`xrf#+sEs{+&)RW@M`aYpB zT4TP$#NnP+p?aVFlIBo-mr!nQ6{-*5ZT2heSJ|(&W1(Awa+^@@5Xzm+@%lV{_Ht`* z`hj<~wJWa0WX67re#PyfE85&g_vhpLgL-_&9fOmb4S!2~Z>w@c+WIh53G9fKVP3%0ohVSSXJO z*(Y-PK)v{2<63pJjy%FQdftib?9#6KqQBqq>k1}+dUA;;dXRG$qtX!FTQ*% zM@Tth9esoSf}@YtFT6^>(B2VBbIVGqwVSB=X24d%bw`}7LYm=7uq}4PN%@Xsp}a*O zpzSbVDn}RaR)bY2AGKKVa-=%a9O>Z|ulI!VhENbfU-1eK)*T}ltUFF{jC70=%9}!Y zTPVDeG!&jGCwN2ttxza$eo&H;k#ps9^RQa2E+UOKOf$M zxA|GyKJsyaA#Lq*2u?9V3r`N#%UEstusAB`Ppmc_RaEoR(w|+5X>H@#jyaCGj(OO4 zwlvmJD%9hI@{Lfw6Urx=k=5fG976fFrIE`WzGfqTB@}K19c5(geeRA*#}Y@CBOsK| zgz|5pd?6H0RSX-hMz{38LYD3x+N2YF%8d10R?15QB{k(S{j)e;s_Cj_#~sinbDxVy zmWr6nIoi3bZPWk3bk4Cs_lK`T{!spZ?GHmV!Q(5dIW=TRJ5m-$4J->Ri7(NfndDA= z%e(hU@dZsZ=Upqiw*5(jO?#Dhb~m*Td9)si4tuz;UHe4MNq9 z@GvAxT@5LYYYb_IbfHEx%;N^Zs#S4mNvYZ%n^bL&jW;o!KeVR;*Q^Przd=vmK}+ul zZOBPV(o!d;Weko2k=lnQsaonn(M?%v4tRuGCYkLp0Ggy2zu_K=i-oupUtcuG> z8XS}2^~R^@=PfzzrVDr6Bh*d}17;<7$HrAw#{~)}uve}f_Y1YH)-mBhTZQ9c$0Njq z#~hC{_E)2{vA^mP>hZytkR()sLd$y{j;9y8e6|_*XCv zsP<4CSo7!Oz%d+t(}tu+^&G9n`;Kpd<9*dx9Q9Aj6ddR~zIR42(0Bac_|fr`<7dY& zj$a+WIevFaPTGrGs2-t43$>e2y9>34P-BGJQ>eX!8oS>asSos>sy@(n9>+jm?HwBE zt9`=*ebxKlf&O1V(096s5>5=%Cp6G^cB5Ig{jSSUTypky_G3ip?Bnb!)P6$k-{|b` z93WKwKj<%80CL7Vz1pz#Zf64T6i^2;Z2b>!x^$*FhcPv9raLp7Lxnn6sBuD#-|HOi z9KqB;9U|0(e|&1-%#pe}b6b!1+j!HZa|}uITPJPzO_$E`&Pl<6zH=f2eKqk9G&XO+ z5!-D0w_fdW7TPMDGo3}Y#m?EzIaUh;pm9(EOyRsj`+icntF7^ zck)`Oz0QTs3a4MFX+q5q>aeC@?<@=s-PQEq&|TZu+}=Zfp&;%&QNLj5pB?KvSL!i+ zL@1`${v9zrxKsS7ARhdzRU~%`J(7cqLocCzq_)h0t&bx&=MW|DSO7p+hd7l=JrwMiX zKOc@CCB{D1I>sKi#vT9g52Fs;`h)4tv&+r#99`{vlB7?yPP%vc+Wnv1p8MVHgKqe~ z(N?{?>F8?bvrZng(>kz&S_d|RaNO}tW=IjAwfB+;=PS;`dH{b-3*be6aK${fHgWu6 zphUZ6-&O#B*ZH3Fedh-R@OPabJ3kR>u}~KXwL++Kf&rXAnXD~rF^_kC=KNQ40B7ny zw^acDS_|OcIKOp%C)9aDoiEgq<^cY#QmYNXi-mzT0BM$XL~?xa)T2K=>UJr5gk~#K za5bj*?}*Stju5bU9o==j9*x;X@pqTXgt2+LzkEq$9BTtS$XmA}PCFSRe1La*yK1yF z`V>FZs$FK6oeh94i_7X_zD2xUB-F)w4Lw{=mrI+PR|<8BHY@MWjr-!s0$y<)o>s|5u9)#9v>gnp|>hBuh8faVW8tjU5#S67o zsH=p!MyS<6KfXYyD}=hbg&(^TT;66sUMAENkM?6%8X?k^?#gftbqy10jZo`^S})Y4 zP0sAD!-q#y&2?xVp)`LM ztk)4OqEK%UD~qH4ak2ix3R!lbEKo8nP+wM|uaJeGN;$R_G8WasO=-5v$I_TkJT1+Imz3YXut$hFv2=~|*K zjh!OY(}jA5P|pSm#`Ud-t6 zG@n~K4 z*PD`qc#<1*A$Dd`-OT@um^llsFxo-6?fg@y0y6nyi}+cx9R~|9Ny=; z$91pkKG*$1-6Pb!Lft1+wgH7YFW1$wVV;t`A)wxjuF=;y)l% z?p9nS)T@PhjZm)@>UBcBUZ~9ZZrtzsOrNy7zK}2IvQzT9zR@P_9QmmIPpG#D!^5J( z9PQWp|2t{_>nH8*2x5RcQm8kFChcxTTXnruitlncTi!cNqS1AjEDT@M9Q8T7`*?R0 zlS6kGH#bUd73yt`Zj;+A)Z2x6=U=wy>UOx@S|Lt$7pQlzyWqGIwh%|oYkv!|JI39c zS-!icyO%pwsCNmKEAQFs?&I#O&GPRR>V5zCEZ-d`b#=$Lp1Pl*o;cT1H2Rs17j=j~ z^RbDYnB}{@Bu#9c^seHcyPs1uYT&6)Ok8&H=d*rjn&rDw-08tNyE{#rvp=9Mx|T56 zLT88Pg`XE1@2gs<9qSP9*9PG1x-0jW=kb6+ZH0a!mUdEU@LZ#zW!%{FEw9sNrfqD( zbC0xDxHH{Zw#DuocdphKuvv<}07X9>>;1PaiKoZ)D5^tMn132>K_X}KgK;c6G!^C+%5g5BfYy+kMvK4BK?BD zCDON+vpJf(>0uh|LVcz<>VK{9V8nJ;x|b2L-Amk6?tr`6eWJU@UF)uM*SnVr^*Ny) z6zcOrJtWi@g!-aTUlJ;}Mqd%?tNY!{^@#0Wt-pHP-JnJ6*TNB-cU0=H-c~t_MVsZf zevIS4551J}&`!BeUp0yUQxCnkw`&1=hfoiP1NJTgw)Wa>4egbJk&*h(=7@cc`+O~! zp6fnOsBZ}M%|`bH?hA$bmQX+V%T`(4d)=35@pqs55;x<#w}twSP~Y9_zTAC<7JuIp z>ihqE{JoZ-dR^>H=eHHGpK#1^KjnU!fc>odIrl+f z^k}=O)E|Y)9;mIt$Sv{jg!)5^4a=)Vw-aiiw-t7}OHmKcgRPGNu4bo!#mm-oU<%#iKk4paMBm6gdg#R`a z;RAn5gg;_O)zJbU5$b=|Lp|r7{clV0f4xP^{kw;EfnDK|JO)pMC(_fwBYPB&>M;uS zC!zi<)L(@9t5AOv>hHoR38O(6Bd+jt@*L;s>^a`k#naUjCcvC)U&3(?=LP2%{p5Mq%tEjGcwCi{Rx1;l(IV z|KPHNQ4X(gc#~2y;#pWqNnkFYo|>A-Jf5|PL?-vi$!UohNhwLp8F$sDx9kpVn9a}NpbbWd4&lQxc7j#^+>N33XUNpqH{mCgw8L8Te zO*%RXE;uFeMFtCZ|0C6<>qQ+OE-E!CB|beRA%hq9q-G?hWF%@ud6W1oB{_lhu(T8w z5_RpRrXy6Umk+hov9z=d)}d0oyf7gx1v{m4MPhPF^O~V&xL#D(a8W54N%0xp1lAnW zQoLzN$tf(EwNq3I>pi?`i5Dr6nzt1sW2rwXYLs4-DO{8{ITg9mai#=BV%UVXt6ImKd0r|7i}UwHI?-~Z+Z$VUrEWD zic(mq;rfB(cVv(D*udJe3aY^ZMbqVa~Y^Q^om#yze=f#cM z`Lw!hGr4)>DH6u++&uEkk-B>3dT@4UnSQ#TKDs;Na0_AVA&i-=7S^=g?4D9jnQgJh z*Swou82buiOt5br)p|R-$M0FxwA0y0aNz;dmOGt2)y$F^l z{yU*y%W`}z2Lt?7Ptkj{RbbB*!k8GEPR2YZt^*rHuk|~IBxG;|R$EP5k=Lr*sTF)y^xpU-2`y{K7xaR`;Dwf7gPY-b_!!8`LV8O)P#;SzY=+&yVQ`lHa3x#~*TVI1 zBanxMdReHKg?d@2m*oi{U(1{DHoOb(OOkaApi?XQwEAE%AP>iTO4g0A2`JOL6~2}v z8+GH1Imy-m6yO?L4b;I>SPsv^>p)pH%Cb?GJp=N9^6X<_JlqE8*#0Oy4(Ql19I%%I zdpRb+B)AXIy@Pxm=$Uv?NvPV!~@u;knW=){R0oVNfrbiM#D!7IRl*G}Ye zB9{}noXF)w2hQ&R`#FDtU*I>~&j8fXr9vm@3~6v8;5V+9CCQCl-NT?B_Q7?4{@v)` zP2E_kk=%Cyb#qfUH+67R2M_spklQmIWMiq0PA1_ zYy@oNX$1Vha~~XrPvBQcisl6P=x)##`T=buIuS+!dW*&m(Z~^92s44Qq6KV+9dH`# z0_q%1{i1J$hX6Z7quXe78~rL!X7n5I1^fu;G8$cWlL7s8>klcA3e>+_22g%C?As0h z>sA2B(G7icn-8T>4hx_H7Qr%D0jppQd@o7eJAeX4pq%dLsQamKK3oVF1G?$H4=#nv zVLx06=&L*W>iz&wr|yryV?bN!{uDd|&jIbGJ9X{;A$%oCJzNk6_-PO9)FS|t*@H5B zTmt0ZgZz7te~)Y7E_f8krw94;cnMyCA0#Qp03D$VcmP|)^nhWI1L!7ZDxjYj^buJ;b1gm<@n^V$K8V8$*3L+*OLX3MfD34tN^yyBKU1L)(dY5ncxD7IPS|Tg+SV zHGB)`HReb78GeP|C8;NJ_KXDV*;9omuz>^6M^6v*hk-B{#=;CJg4s|H>tH<~bI*;i z1JGGdbk-BQ^h9qx(Ob`3;6Zo^UV+!(b$AorhIiq8z#ct6hF{<}y}RfY!M_~l{ zKtR6!$k!kJ^-qHgKu7)805;dR=z*N9i17-j+444b^0UsK$0Q^u10jL4= zFaSLaz&{6UgiWxS;Se$p#ukII#b9hPm{=8;3*;Y1{&Dz1+)SVjac{!g@GiVBN%52y zzXT{F{zO3E@&AIK;1~E!k`P@Qg0BoY1x|%6u#F+u7w{`!*F*zE!WuxP#I3L$PLrgh zPT&A+orJ!U&{xt~un#T;Y?riOlG2egow}#{0Q;v?|8(R`r=0Y!;2Xe?GOB@cGIqh~ za3-7$=fe4b9WyS5Mxcxg%E-6_?t*&&`(``<=qv-BWuUVRbe8cHJOkJ}tPkK@!v?}&z_!CqfKiYIIgkfqU@XjlIWQ0A19crnU58QE zVT%Eq4m$wYbQtmuL*8MOIqYG06ds2s;WhXWJ_d9%?0ZQX-T{sSX^%1?HFARb>z)41+s}X5HeMgK2 zd~XEx8bQ5AEP()=2>93te0jt&SOLf~Vhv!=5!iDC_8fsdM_|tp$T;F6*aLgvQn(!O zml4Q2;sZ%Kp%b9{6DGq_;JOoTgO}l3_>F0T0V09Aj3oAqbO8Q1(gW0KWDn>G@jx3L zi5*8$myyE(dyd4OBeCbmVps<|09i*O>qulBc>y5jNaP%eoFmcc$eZAPcnqF^r{EcQ z5ncgwJn{{AOOi%)gkdlTd{6@?!zpkYkpHM_fqX~Z0rlU~d&`B14ob@z32hYO`@D6+kpTNK1 zEBF?EfS)8On>u6tQ1xPxe;W4cIOF95@dyfQNxH za$L|0hCmjej~sN6GYgggdFGI3jsWt_A>W)Wunn+N&Mv?Qa;}C4fP8bPYYuhIc?J#w z^~`x0UIXl(^EP}3Kf=%OEBr1=xe?<=m!D4j7A@$k$LpzK%S$?b4(Y!rg$+j(Gqcf=A#zNyG z3Xj86@C>{GpTZaL6;RJ{Kfy2XyCjV_KonsA@rgjWN=jfj;F5UvDJ9$Iesim zfXRTp##g{1K)&%+a3a*fQdkbF0DF!{_VMS#9q^_kO`yyPeSo+#ApqOqDnRZD=wbr8 znD7d`2H0)FzuO6^jCz0N{yAtcH_d z8=MOl!5-KLSHJ^A8McnjWz58z|?3_b_!HHo@T`T>51-y~_W1?=DiHz4EW z9zYD7j7}%_gMly@;sG5`UIgfP^0jaXeqsr(JJ1%VP?ss_W6C->8PLTPbTMT&oDax9 zr4cTH%V9qt{}kk(ave~IDfrzK{CUb_@H9LJlrsf8PWe%irXuguSU|3+agYE>Fcgqs z>L?&4OvTTpE&!zp3Og6`f4|6uyz90(_^y3glTpo(2735Rh*H{#$_m7NkK2 zjDZPI2t}{}76WAzV7G#LSOzNryA^DKv*9X0R|Pk~&2Sss3HQML@DMx-PXM+nz;*@i z!-s&Z1=z0u`xW4?1z*E=fDH@K`!xJ>S}M!}%A9sFpu1_GNYeCM*?q*1`rj1vbMD*ac_6S#UeN1nF9Ghel;DxDnze^ksw1n z%m)GFS4bX(`{7Es3D7~|3qYQQ&!f)Yw!1gnYU;{TqLl5W;eSx~p z7zo&EMjp(Dc~Ao7un-mj_L@OmXVk({SOME%C+vnZ02ybT3l{)7oq^BJxCAbPD*zqO zz(zCC@ytOm3F_e-xD}|&O!P7HS4k?809_QJi=r;z02g?m8&HR$o`9Z;k^p-aWxxrL z2{}N0ippRkAY&1JRdgO8UlFnuAzKl$6(L&@5MF?n;Z1l4kiFcR- zhJJvJX2(GSP~X|qcXkF$1blRM0ZfM(fM3s^3&nsPXP3hQsDP7UA5hQPlsTshB*RQt z24}(zKwF!058!8Wkbllo@FJj(Ij_QDpbm51f`7qR@GblR*pK3+x!7;64E-S&ra}?S z0c4w73Vxt1%?-ebPz&e7)9^Wv-#qe|=LGzBUM!%4dD%do^T=}^b)HAQ^T>A|^_^D+ zK3E7#VLfbu&9Du2!RbI<=ba6g!Hw`3JPFu;-a)|r^Iit*HIKT^dmG+^58)U1U6P6o z5DCawY=q+gofb!d1#E!t7Ng^0Y*dVni_d|(;9W_YuR>p-F7wgH{KZfO=wd#)n726w{Ufb8>slcW;lD=`Cdm7voSe66Gx z^a1Qtf}fQPhUst$Tn^+{LLMd0!Siq!XagnapoBb2$+MLDmXdEN`Icg*(rD-bJpp@_ zCc$tx0mi{(mT?1D1@83+Bhu7!u-QFt7% zVd>L=yrqW#zb(a%rP#6bb$Anyy^Ok-jReXpL&s(4uMFFl{acdCj|X&7j@;#`kO9LW z2PVKIm;wbr9m?^+@^ZkR%kk&(0Mr5YE?)_&;Uc&b@U!w~0bQ0OTlp(MJ1WN?%0GaQ z;1fymSs)i^yFT*sk%w;w?1Xax9r$hs^7N6Xk2?FPGjB?eeAL(XDjbG4;9u|){4Plg z4A22O0=8Q~T^DqP?l1^OK{n(;K8%M+fV~z_*99|SHq3)sSPIKwB_QL1wXhC0z(zO~ zw!k*PW($yY0rD)Q%!Nr%1lW7w`EUmy`@*;3J@^nlhOgjf_!X!_1$C&11R2cW05^04 z{JEkJ;LjDh*1BdEo^tt#$ z_y)d*A0?@BDilL0lmm6Hd>ozvWLnZ6QXn0M!Y)8>OVHaA>b7J*TnP^WKE4DWUvda& zJ4=v#$tRLjWd~%cLZ&KgU)2v1VI)v~)ifxCnJ^3Hz&w}_$W?`0RoJo$AFiT4Rcm1# zoD7=)|E;2KRi^>+RUu#1#jqDH1!S#4-&ME5?SMV2&}S9CTZJ71=rCXf8z5`I30|PS z0emY!n+s&Y1VGLJG6fbx6`TmPvp@r2*TBs{IRX49fGz{~!UKSQ0*?T?4WQcqF(&Xl zVDG?7@G86xZvj3QK)wL-1wMn%0lNpVTQ$0?9td%e07;Mv8895sX*IG|=fW5m2NPim zpy%otFbn3wd?Bo`P7;Lz1P~o1*EGbT`lQqNmom{x-Q^>Zh#Nf^#bIo zqrAEuK>E7Vfi_W3ne~)e-xbKa9$%=x2=>5Uz!#Pdf^@*POVQU-^tJRpcp9FCgMdwz zWdeFzHW{YEbil659s=yR>@j#!l9s0c`d*#`d5{m-eK~esu6<9w%b$~^6*IvHD`5># zhZXB#BcQ_-TY>tlK;{+HeFb%2aTQz(*Tao~Z>_iu?u5JHUU&eIX$3N^cu$g6VxyJ# z$V%i`xdhPV%3I(?_*RlunE`#S>JB|276t?LSd|9&(W+rE0?^H>@h}CZ0lu?p4irNv zl!JhMa2+7uDs;38-(Q7ntL_J6TZL?^D1X%}@B#b_zJRacJNQ+SR!e~Ft7T9j0Vrej zdO$a;&w(r9YM?%=AB2~HJXe$F>Q8}uSCj8*>bd%R_z8ZIq&4`(8Yjd7`L0QVRG_YF zh6D9ngALc@0&Ql^IG7J*;Dd$Whf1J*tT_>CVJR#JY`W$wxEWpr$~-9s@Q0HY0rowK z7<3Z4I0=6^34b{05WEO)!bk84dTuFm@Vg|f?EvUwE$wYBe!Uj^t+jyzl3@m5 zhqcJJb}>`|vaLn7wJTs1Y=+%%I-Cjk*4j$|eXhL*@UOKu!JTjq&>q&J-?b0J50cd2 z0`${>UK(ZsIZTgZA7+H@UK(wuT$`^Q#yeQqG13eKoX<^{&dPH$bwuL1K94A#c(>H zpHtAwDTm>6piZYy#wHcqK%SfWKq6qTP2{&}IAj98uxT{p1NGkIgOx!2HZ=gc+O!c) zg{`myu-B$D;B2@Gu7&FX`)#@zZi74FZor0{sQ;#i;1PIFl1`0;en6S0;v1);yHm0I zsV~B}lC;?j9>8XsdqOM>h7?GLVQ>N<|K@B!C!49mX7bxy2-t4(JSc$$;D<`6f-P`0 z+yTh8`F=pQ&G`N1CjmWfCMIrv3*H6%YV-GiJ~v}CE|<2bfW5XH4_(0oLxD23Y=bjl zFQAJpx59n!G`tPuxrIEpP|q#oyM=tW;G-&&y1q`?2 zT(|%(f-B)Vpq@Lh(+>P$$6atQ;1fHZgExS>?x3zasOt{uy5o0AIxPZZFv4-r1x!F4 zPa6Q}^0YV@0*R0U*z&Xt7!LUEY1s3$xquE%yBH|*v=1a{XJ<%&Nl*pIy%SyRJPVM2 z=lO66Tn*R4^+3DZNga0LYdaqTe0k?nfc2cQP9-){2Wy%Bc9+b7-dhWgjZilA$1pWd_9^S+H&t8t9zmdOk7QKzs+sMChYoiL{wnmjBips=LgZjvAR1=!foc@ep5$<@@ zR_tSxyhnLsl$l2tLe0@?j*dj#(dv$_PA%%9_UP8!%j3vtv{^?#hdD>Tgsesn=1tz> zUCcLHUZY1cnz2k^5>xnz8Kf|qbo4c*DBc{C#1O`@0{1%RGPi?ZYzTRb)&JOVDo}~L zse-+XHN)7NG^GV*80+4~#?uuUj!onRd{1M?FpI~TV$_Gi|zlYL~7%~4LE z_px#td!FBdU|f0ZY@8j9vzu`r@;yJ|opEZ9Q*)e}5DOjA1;QO`eN8Og3xszje);Y}Vw%5KMBXlL}y$ zlM3UuCzT)qSxzcP1tN)}CHk859_E>}fipocxj3?&oWS$E%Ige4|C5LDCExHJKcN4~ z`ky=ww>(+@ljpOD<*ejq+}~tbO}-NZQ@k^!6lHN|Qz{Wn6=XD}G3}7mlzVYoQ+na1 zrs#W0KOUz)pW}@wM>xq}K`_-WrrO2SaLhZkA!<%-MH}4fRQ0C1xv3AJ=F~?~bE=wC zU*TQMIMs|(%{X-|6PUzQrjvqOojQ+=Y+)UBEzX!WTVfia-4cS2!5(aTfF(x z$IK#w3qdfg7`4&&v}VL1w`rZQk7@2}T5s;d4Abmp+Ed7F+CW}rFmEEeY40$Rm86r& z9`rWNeND?H54SbVZB0891k+2=glAB1x*F5JVK_3MZU@uTP;(D zhWTdbZN__ifIFS>37_&gU!m_A`kpbD9sCglGrc+U9^BK+4@u@{4r329?O~>GZ033X z<`()-(SJ&Q3KB+HWSJ60WvWw)y41(LrM$%3=r2WoDe_LyTZ-ONzQwJjOu}uY%x4)Z zSj}48R?1fNo?^!-yZI{!W<}Bi`OlL7Ec=)>l#ej;tjVZ3OU+qJP&2C6zn$nz>w5BZ`=tO4*ps(4}@aF7uK`MKF=y&3WRMivIr#QoT ze&Y`=qVH6F&nrU{9>SaRCa{5DgJ8ZJo?o4g*u#8#n4iS`^y3+xXCN=5|M`RYkWbM6 z{4W{K_l#sTOE}0$&Z4*ZzjKMp{DV7NP=sPsz+M-`P=neuMBfYKwjd67wcsi2Yk~bN zu$Kip$mJKjv%tIy)m*6NLNyoOjk*ifU091c)JJv;<+aev3;Uz)!e^1w!a=;kYrMfw z-r;@BxX_FXM>7_8x-gk3{KRxpn2kIaZbLSU+|43yF1jBXERw+@*)BTF)gV|L;9Fbl zTU%V1QbeNv#Z{MpHBG*zjNtd^Q{X#*P3lzU0Uj7!b9^gbTsVIJi%`ZIv1`3QY2-H3UX zxr=2Dd4R!;U>?g^$9E#QV~6^ipDp!sv-8esy$ulP7iwX5RdQ} zPw*sP;Eh#BImN{wSZx=p?P7HVchd+pSGVR~dZOOy`*EkM)m*LSYBg8C%sUL{dq!fu z)#I_N)l-T5ypa|PPq&7c2E3OhL; z1Z(VQO)d1jrYX&7K}QnM|C%Hoz)sfmNvy zjdfr1EqY%kt95p~?$01t@6Gl0x?b+jyKrxhU-DF z(Hk3kBL9s~V;39cy>S@k-8cm`H>$aDDe7)icjH>tvxzMn;5b+Lx9+VV@JGR>{K#oj z5zM(M96Q@oj=D6!jGN53$&GHZ%S|n5ODuf@Zkk&4U@v7{-&# zWM;7l{clcV6>C_BeQe%~{x=^YhhI3ssUX;rj~X-~4!v#hy>018JYDf`!Ip=4hC#f7 zthT(tyU1+IFh0gjZJCER(nH8U{VwbxT^8y0VBYjTsG0sGFJrcJ_0os(9%`nmnXYE~ z_qe2gmuU;0MuDt#L}k$t*()7@pd{iR>vcmCuOSNI1Rrpqw>P7rL>=hkTC zv2_66+&UAx+xk}!Y?Hw@8E)%=zPI(IACL1K|G_@Cz0RAMVVm!5+oybmJKHvbQH*5* z$;fG2F8&?aroV0a+jbefZPVNK0Qqb$MOms+n|j33m=?4~@7p_Ix7+P;`$*Q{yV$-T z``CV#ik_m$BA z{bw|xJ)O~iMgqOLpFX&|jCUB#H1w6>W-}I|rwqAd*jK<&)!9ONkS&&=ajm+W^nBaT+cf0y}onQxcgc0EEro?rk^^DHl*?_K)d^#yX-wG(gdw$t7A zzuW$Izsoq5V-LISVfPlcvWLUu@Czr<|L!wf<~sWKW5J$+6s8!m+f$dGJW79_;u+l6 z9zE@Og;)8Iula#d_-^*Nt35yAF80{(9=YvFB`XN_dSh=LWWQJTd+lPcyWRUZ=H2@~ zYVK8Y?+Dc0tM1ru?oHHTgSN|*nbyYcpbg&-x~x6@?q`+ z<~=YBeILkV7rXiQXO5xY19`~mz@I^oC6BC3J(d&%mLEVJw)%Pz9)BFiqa>>|r9vW75;49*0>!Th*|gX$gh^9MgBnHB8kk03Zy z9)EtwJBJ3~P7XbfJP*B$J2^C%FR{-v{=q$Fhf$a!m_6IKoLvz!XGdY~ zZ2e@{!@SwCw5APhX-^OIlidqD$bKB(SGL)+^_Okl?Ej#zY<*>a$VUvreA(YIg75i} zDabMVC#ECs?5%8LJ3BeSpZvu|{tkk}A>@8IKLsg?91oYp&JNe2G5SB;jRf?4SkH&$ zc6bRtwrt;#+)IM@BM+ajf77 z$GCu8kC@}gWn^)?!_bnqZ zU*2NOmA8V`tYrh6ar=2Y$Yc+{a)$H##vizyyvw+qyc_8CxbNY3e(dPDK99HKVa$Ad z80I=|ug8~?ftip06$B@mAiER2=);q|h`)E@CCqT*znI~~=X`}(PK;(86H)iXPfTYG z+58;@Cu`Awdx)b0-AJG(N!Z)T{yc|wPkQs@e;9(dPukzf4>7~Z*&GUjQw1o5Tuw!D zH&v)cUFy>ivz=;2YdYgDPMh&`U);rM@0|9|>G%1P;e5|X%y!yrr_FYHDsxzbtWNL7 zjhxQHtf!A+pQn#=3VEHrhHOsD#(x={k2!6!7=e&E)yXU-n&fd;##_gVu zpds<7dtTl1gLsA4c!QyQ#K(Nf7krI3&QE6rd-;v)*zft9*ztvm*yV*f)W=&FnqZa- zy^;9^{a?`k1^xe(%uHnY*TEpTD5HxqxM| zx>k-D?DU$QUb_c1ueGKf9qC09_tS@md6dWLkNdo4Z`T%M_G>x(69m@_V#e!wzpnS| zdcUss>w3Rl7qegY?)ApV@_HOCxsSI=MZN2Pqt_exQ1gcG?1r7(sDi(LL%%oldqclB zI`9ZDA?q7I;QP3-flZ_%(;GXH(T%-9aI+BMRG<>k*zL{gxVxK8QTt|Ry3rjwzd423 zsCQGJH-E!kZ|e2toglcS-&@5{_m;Z1?nckI%zaDdx9t0tec!UnTTi3sTcdG*xAb{S zUboH#!R-=6QVspwu0wsye%tJ~&3;>dw`Fx(kGH$x`@21yWk?s)S~ zE81dTce-G%I|-QU&Jf1&6LXo*B9@ZIN-hPVpaD-Zn#rWHfW<5$jg=hY6zBMjKe^0R zu5lv>h01Ubop^-D=+9F;%L@$R6<*^FhVl+0_>obJWdf6!%5+kg!#ozEztA?W1fhI| z@piuE^g`WypX2BAWs-xw^IhWKpVx1`o7@RP`TgDe#VAPx>f|p+H0tH=NFse1z%xA0 zKwf4DZ()Y~@9_a2k<2P`v7-V72%{(^@K%A+)TANJh@%y4=uQ$3@DTP{Ku!go#x4uI zfF25XyTJb#!xUz-oHeXx6Y1DPfj#Uai$fgeJpTltf<-7!IQlAR2L&roiD;_Q1br5? zmx3+OYeBshY)1#=Ua&KRkz2uC+zvv8>Y)EZU$K(S=&8_APH_f(6mnOEt_PtodkV9s zFnbEKr!bj>m8T;16{cQTee5kvPhoB@%aQN>i4*s7@{Hxu`uCwb!C@F4`Ww6-{LsJF&Z> zr}>kA>)j4Q#X|Js8D3>5A2SknQEVDBm=%PIm!lqjzPOt%{yrb!=Zk;J7ktfdzDJGX zV;Ii@7PFM)tY9^3v4i4vP<#tp*^WL-6rwIY&|8U7xXBW7FJUJo^;0sMs@Oxxn#ieS z3tA(el6FwCGhKOrzUaT?6Fkjx$g1QZzGV(+=&R%g+;#pq6Y^BXs`oDaF`AVCwwE0TQuJm{Oz(_`8zS8@+8idMJ zq%ATp^BFUcZJFbo=XdPA%q4CHp|S-jOfgDg*0N==`?59Af7yC8qA6xB+XA;x_AUO$ zmwdx;end}Y$Krb^JDcUKVLh906J;~8`?CAV;!qGO7s4Cm9^`QbVi)B;XuWtoLtLoWd|7?<#rG%Uzr%pQr;}(%~C#=#xx@inUsG3nUt4Fd6|@#N%;Zj zq5SI%;cebSKjnw;_o1ImpSh$Kp~1y9=Y8WMKse{ z#1fXVA_!IJfcvT7PAWXh3k>2FUgHgh;@hZTXBFH}1^cQnhH*?FnJG-eT~tV64ynw? zH&Nky5US|rE80&*8CQIlN!WA69IgkU$o%L%Qty68awxJacjIOvt5K6W)WfYrwnhJu z@!U%yy|DL4yNz_ukrUBZ~rCzb4? zl71`6rIMPJ)bx9kLzV2alDd`VV$MnnkyWJ)>|#GzWRuG;oZu9H2cf8ZRKr|R=87^` zR4jHB)r{t}I|3pha21qLU%vOi+Jbm|KM)!{xt|ihf$OgL{NtE*kN=fqVWwx+h??# zqU97FPgi6WeIJkVINpdhOZ0QRz?;0qJG@UabC`!6N6RldlOyC}hG;WH|H&oH5N(Fa z1u0B1^jY~0+-YThud-Pyo3ZlkAXFuU@1}~Gs+g&YpRHo1DrTx;rYf?jQXkn=kxdoZ zRB4WEsb zU;5$w7tU!`2_mqQYULP-Z>ySbw%QD41tGuRI8@!ft0&S6x8wI0hpIn}8>+6?>QCZ^s_VJ> zi+sQ^KIVVOqq?4}>$&=HM(`t}7{gNbqrV!FwBs2*$F6H^!7Mc{@CO(98*|hwL=lP; zPHDSin)X@K4r-2O921yB8ou3{{%xtbnXT+% zF9$e?+-vzarIt5p_2CHyVHdSN;tR}MYZhwOQnQw}4eYD7Z=iN(x{*LndefKwJjJuTz(9ub4(_}5hm0o$z12=-0eY*g zpW0@v?K`TypFB=-nzKQuj@orz;uT)yb^iVDUkO5Wi&2tN$fWLF$hYp@RG}I&s#^y$ z)HOrhc)DPYy5^{Bj=D+QkG}d2zP=|J-eva9W&PZ40l}5EcMJ%Z!t?* zLps~ZU^n|Xz;Vn~?;OA3&gxypUDmrEgzD?Nek=4=-?vczJ|09*^`Bq>?zaAO=%M}y zHez4()vK>YgA%yY1{Kh6gBGaSK+Og{P`81)4IV;v4f^pocHH1?KEORS_?XZ6l5ZGJ zGO1*;2YEF($YI=5gI_qouh>^?J_=G8S;fjKHUc*mE1%eE$R}1lvGvevY-`#ft62TU z$|CkL%pYt1*jIQBcMvP1SbfHR$`^cvJ;d6B-(4Mwy&Z%ahIpD`%pnbT*Kh-y*^2vX zXr6}VX=t8?*{IP-9*y+U$etQ`yOG^DdY%^0RO_8(_oarPBg4mTWEhd$^jPET=m9`_utp=R6|#-oQgJ;a$kZYHxyWdYmRi93wj z%K;9dkGK|54`){OGNXzS_vTjh(hBg`C?o zpc%fqHmz`@Z93BpeYfdBFJ#`v8*R3;k6*BhHgaoYmbS%+M$NWrwrzsCes6lHtr^?4 zLvC&3xu1`5Gi_%w8#mL|&9q(2GFGq}`)_OiZSB8pE_s~fSKLlpS+u>zP2|unL`lro zPS)-2LI&+x&>Hi#>p)ME&`Y~M*nd0wZ)gAQ24co`W^8vH@3lJ}gxdT0_K!1|H+hS9 z`G8@3f{faK$v4Qm{d7_=V|)3umrwhJxP$iQY@fy|er7F4xe-xQTSO zvlBgZyc&c$)gb}(I;qj=b?l^*U3AiKr)1RZq-Li@sM|^1PBQJZhV^V@FUPRsPBQIu zmf!i4OI!{@@dapxoZ|H!ulIPp#}D8co@XF0^C@5OHN*Lyk&H$^@oqR?4)OLGudn!} z=quiw@#c)*hB@O;agl%D|A!kvsB;M-xf}WTed?jk4QYb@I>*tOZrDxd9+xq z-XPS)&vf|!d+L(R6s9qg+1Oc^1uVvmbnz{8(Nhvr_kbvOISA{%$uRqd|lkw;f~bnDJTJc10m>9Ly|>9&py zLFnF!)T9pe_=FMs$SD5cCU=5R_YjZrJhJQl5}R>f-ObhA``vSJ8{Mx3p@e*xDWNb$ ziJ}Jgp-#f%JjqkMj5|n>dxHHY*l&XUCd|O>31&_(bAp)@{^D=0Vs8nzf>2^We!Q3H zy~L_irxtZ-KqKUy*qTms!FQIJNKf>Xs3*UdJd~)X#Q!juHyDcDB!0#he9c%UFbVx8 z&cpYSxCJ+vD1X1FJd`N^MENJmKhd5NWt5o573?YT2LH}r$35(y-HJ-YS?ct-&3zvw50>xNWgx3^+xu+-oYEaGRWct zcG1f>+dG7Ldsjux-fH%~2X%X^+q*p-=}cGpAgkVk_?XX`%p!K6|K5Ap&p{4jXT48x zfj_v&Wv&LHqypG|lHDg2ry}k)DH=Wbz2l*z#x$il@#rI|JMJi{FEU7af&o0kbC@|v ze@XgF`igJ)j)~ZN(oANP$`Y2dg4L{HE9OYbMej*xIM46sJ<0BpuH%l9+|hldh@=*^ qsY5+l(1ZTi+kJx=%6ohs{J;Mc@PD8G_diko{|^4Y|Ag-Q^8Wy{uCAj1 literal 0 HcmV?d00001 diff --git a/IDNowTest.xcodeproj/xcuserdata/kristianrusyn.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/IDNowTest.xcodeproj/xcuserdata/kristianrusyn.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..8d29619 --- /dev/null +++ b/IDNowTest.xcodeproj/xcuserdata/kristianrusyn.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/IDNowTest.xcodeproj/xcuserdata/kristianrusyn.xcuserdatad/xcschemes/xcschememanagement.plist b/IDNowTest.xcodeproj/xcuserdata/kristianrusyn.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..3cc2b5a --- /dev/null +++ b/IDNowTest.xcodeproj/xcuserdata/kristianrusyn.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + IDNowTest.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/IDNowTest/AppDelegate.swift b/IDNowTest/AppDelegate.swift new file mode 100644 index 0000000..1066728 --- /dev/null +++ b/IDNowTest/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// IDNowTest +// +// Created by Kristian Rusyn on 22/09/2024. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/IDNowTest/Assets.xcassets/AccentColor.colorset/Contents.json b/IDNowTest/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/IDNowTest/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IDNowTest/Assets.xcassets/AppIcon.appiconset/Contents.json b/IDNowTest/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/IDNowTest/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IDNowTest/Assets.xcassets/Contents.json b/IDNowTest/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/IDNowTest/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IDNowTest/Base.lproj/LaunchScreen.storyboard b/IDNowTest/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/IDNowTest/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IDNowTest/Base.lproj/Main.storyboard b/IDNowTest/Base.lproj/Main.storyboard new file mode 100644 index 0000000..a7603c0 --- /dev/null +++ b/IDNowTest/Base.lproj/Main.storyboard @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IDNowTest/CameraViewController/CameraViewController.swift b/IDNowTest/CameraViewController/CameraViewController.swift new file mode 100644 index 0000000..51ca35b --- /dev/null +++ b/IDNowTest/CameraViewController/CameraViewController.swift @@ -0,0 +1,41 @@ +// +// ViewController.swift +// IDNowTest +// +// Created by Kristian Rusyn on 22/09/2024. +// + +import UIKit +import AVFoundation + +class CameraViewController: UIViewController { + + private let cameraService = CameraServiceImpl() + + @IBOutlet weak var captureButton: UIButton! + override func viewDidLoad() { + super.viewDidLoad() + try! cameraService.setupCameraStream(view: view) + } + + @objc func capturePhoto() { + captureButton.isEnabled = false + cameraService.capturePhoto(imageClosure: { [weak self] image, error in + self?.captureButton.isEnabled = true + if let error { + let alert = UIAlertController(title: "Error", + message: error.localizedDescription, + preferredStyle: .alert) + self?.present(alert, animated: true) + } else if let image { + self?.openImageViewController(image: image) + } + + }) + } + + private func openImageViewController(image: UIImage) { + let imageViewController = ImageViewController(viewModel: ImageViewModel(image: image)) + present(UINavigationController(rootViewController: imageViewController), animated: true) + } +} diff --git a/IDNowTest/ImageViewController/ImageViewController.swift b/IDNowTest/ImageViewController/ImageViewController.swift new file mode 100644 index 0000000..6314d0a --- /dev/null +++ b/IDNowTest/ImageViewController/ImageViewController.swift @@ -0,0 +1,153 @@ +// +// ImageViewController.swift +// IDNowTest +// +// Created by Kristian Rusyn on 22/09/2024. +// + +import Combine +import UIKit +import Photos + +class ImageViewController: UIViewController { + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var descriptionLabel: UILabel! + @IBOutlet private weak var priceLabel: UILabel! + + @IBOutlet private weak var imageView: UIImageView! + @IBOutlet private weak var activityIndicator: UIActivityIndicatorView! + + private var viewModel: ImageViewModel + + private var cancellables = Set() + + init(viewModel: ImageViewModel) { + self.viewModel = viewModel + super.init(nibName: String(describing: ImageViewController.self), bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupNavigationBar() + setupBindings() + imageView.image = viewModel.image + viewModel.fetchProduct() + + // Hide the activity indicator initially + activityIndicator.hidesWhenStopped = true + activityIndicator.stopAnimating() + } + + private func setupNavigationBar() { + let downloadButton = UIBarButtonItem(title: "Download", + style: .plain, + target: self, + action: #selector(downloadImage)) + navigationItem.rightBarButtonItem = downloadButton + } + + private func setupBindings() { + viewModel.$isLoading + .receive(on: DispatchQueue.main) + .sink { [weak self] isLoading in + if isLoading { + self?.activityIndicator.startAnimating() + } else { + self?.activityIndicator.stopAnimating() + } + } + .store(in: &cancellables) + + viewModel.$title + .receive(on: DispatchQueue.main) + .sink { [weak self] title in + self?.titleLabel.text = title + self?.navigationItem.title = title + } + .store(in: &cancellables) + + viewModel.$description + .receive(on: DispatchQueue.main) + .sink { [weak self] description in + self?.descriptionLabel.text = description + } + .store(in: &cancellables) + + viewModel.$price + .receive(on: DispatchQueue.main) + .sink { [weak self] price in + self?.priceLabel.text = price + } + .store(in: &cancellables) + + viewModel.$image + .receive(on: DispatchQueue.main) + .sink { [weak self] image in + self?.imageView.image = image + } + .store(in: &cancellables) + + viewModel.$errorMessage + .receive(on: DispatchQueue.main) + .sink { [weak self] errorMessage in + if let message = errorMessage { + let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self?.present(alert, animated: true, completion: nil) + } + } + .store(in: &cancellables) + } + + @objc private func downloadImage() { + PHPhotoLibrary.requestAuthorization { [weak self] status in + DispatchQueue.main.async { + switch status { + case .authorized, .limited: + self?.saveImageToPhotoLibrary() + case .denied, .restricted: + let alert = UIAlertController(title: "Access Denied", + message: "Please allow photo library access in Settings to save images.", + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self?.present(alert, animated: true, completion: nil) + case .notDetermined: + break + @unknown default: + let alert = UIAlertController(title: "Unknown Error", + message: "An unknown error occurred while accessing the photo library.", + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self?.present(alert, animated: true, completion: nil) + } + } + } + } + + private func saveImageToPhotoLibrary() { + guard let image = viewModel.image else { return } + UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil) + } + + @objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) { + var alert: UIAlertController + if let error = error { + alert = UIAlertController(title: "Save Error", + message: error.localizedDescription, + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + } else { + alert = UIAlertController(title: "Saved", + message: "Image has been saved to your photos.", + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + } + + present(alert, animated: true, completion: nil) + } +} diff --git a/IDNowTest/ImageViewController/ImageViewController.xib b/IDNowTest/ImageViewController/ImageViewController.xib new file mode 100644 index 0000000..799c365 --- /dev/null +++ b/IDNowTest/ImageViewController/ImageViewController.xib @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IDNowTest/ImageViewController/ImageViewModel.swift b/IDNowTest/ImageViewController/ImageViewModel.swift new file mode 100644 index 0000000..a994dfc --- /dev/null +++ b/IDNowTest/ImageViewController/ImageViewModel.swift @@ -0,0 +1,60 @@ +// +// ImageViewModel.swift +// IDNowTest +// +// Created by Kristian Rusyn on 22/09/2024. +// + +import Combine +import Foundation +import UIKit + +class ImageViewModel: ObservableObject { + + enum Constants { + static let productsUrl = "https://dummyjson.com/products/1" + } + + @Published var isLoading: Bool = false + @Published var errorMessage: String? + @Published var image: UIImage? + + @Published var title: String = "" + @Published var price: String = "" + @Published var description: String = "" + + private var session: URLSession + private var cancellables = Set() + + init(image: UIImage, session: URLSession = .shared) { + self.image = image + self.session = session + } + + func fetchProduct() { + guard let url = URL(string: Constants.productsUrl) else { + errorMessage = "Invalid URL" + return + } + + isLoading = true + + session.dataTaskPublisher(for: url) + .map { $0.data } + .decode(type: Product.self, decoder: JSONDecoder()) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { [weak self] completion in + self?.isLoading = false + if case let .failure(error) = completion { + print("Completion error: \(error)") // Debugging Log + self?.errorMessage = error.localizedDescription + } + }, receiveValue: { [weak self] product in + print("Received product: \(product.title)") // Debugging log + self?.title = product.title + self?.price = "$\(product.price)" + self?.description = product.description + }) + .store(in: &cancellables) + } +} diff --git a/IDNowTest/Info.plist b/IDNowTest/Info.plist new file mode 100644 index 0000000..fe0dbd8 --- /dev/null +++ b/IDNowTest/Info.plist @@ -0,0 +1,31 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + NSPhotoLibraryAddUsageDescription + To let user download the image + NSCameraUsageDescription + To let user make a picture + NSPhotoLibraryUsageDescription + To let user download the picture + + diff --git a/IDNowTest/Models/Product.swift b/IDNowTest/Models/Product.swift new file mode 100644 index 0000000..664571c --- /dev/null +++ b/IDNowTest/Models/Product.swift @@ -0,0 +1,15 @@ +// +// Model.swift +// IDNowTest +// +// Created by Kristian Rusyn on 22/09/2024. +// + +import Foundation + +struct Product: Codable { + let id: Int + let title: String + let description: String + let price: Double +} diff --git a/IDNowTest/SceneDelegate.swift b/IDNowTest/SceneDelegate.swift new file mode 100644 index 0000000..7cf6cab --- /dev/null +++ b/IDNowTest/SceneDelegate.swift @@ -0,0 +1,51 @@ +// +// SceneDelegate.swift +// IDNowTest +// +// Created by Kristian Rusyn on 22/09/2024. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } +} + diff --git a/IDNowTest/Services/CameraService.swift b/IDNowTest/Services/CameraService.swift new file mode 100644 index 0000000..4ed5c11 --- /dev/null +++ b/IDNowTest/Services/CameraService.swift @@ -0,0 +1,89 @@ +// +// CameraService.swift +// IDNowTest +// +// Created by Kristian Rusyn on 29/09/2024. +// + +import Foundation +import AVFoundation +import UIKit + +protocol CameraService { + func setupCameraStream(view: UIView) throws + func capturePhoto(imageClosure: @escaping (UIImage?, Error?) -> Void) +} + +class CameraServiceImpl: NSObject, CameraService { + private var captureSession: AVCaptureSession! + private var videoPreviewLayer: AVCaptureVideoPreviewLayer! + private var capturePhotoOutput: AVCapturePhotoOutput! + + private var imageClosure: ((UIImage?, Error?) -> Void)? + + func setupCameraStream(view: UIView) throws { + captureSession = AVCaptureSession() + captureSession.sessionPreset = .photo + + guard let backCamera = AVCaptureDevice.default(for: .video) else { + print("Unable to access back camera!") + return + } + + let input = try AVCaptureDeviceInput(device: backCamera) + captureSession.addInput(input) + + capturePhotoOutput = AVCapturePhotoOutput() + captureSession.addOutput(capturePhotoOutput) + + videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + videoPreviewLayer.videoGravity = .resizeAspectFill + videoPreviewLayer.frame = view.layer.bounds + view.layer.insertSublayer(videoPreviewLayer, at: 0) + + captureSession.startRunning() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + if #available(iOS 16.0, *) { + let activeFormat = backCamera.activeFormat + let maxDimensions = activeFormat.formatDescription.dimensions + self.capturePhotoOutput.maxPhotoDimensions = CMVideoDimensions(width: maxDimensions.width, height: maxDimensions.height) + } + } + + print("Capture session started successfully") + } + + func capturePhoto(imageClosure: @escaping (UIImage?, Error?) -> Void) { + self.imageClosure = imageClosure + let settings = AVCapturePhotoSettings() + + if #available(iOS 16.0, *) { + if let deviceInput = capturePhotoOutput.connections.first?.inputPorts.first?.input as? AVCaptureDeviceInput { + let activeFormat = deviceInput.device.activeFormat + let maxDimensions = activeFormat.formatDescription.dimensions + settings.maxPhotoDimensions = CMVideoDimensions(width: maxDimensions.width, height: maxDimensions.height) + } + } else { + settings.isHighResolutionPhotoEnabled = true + } + + capturePhotoOutput.capturePhoto(with: settings, delegate: self) + } +} + + +extension CameraServiceImpl: AVCapturePhotoCaptureDelegate { + @objc(captureOutput:didFinishProcessingPhoto:error:) func photoOutput(_ output: AVCapturePhotoOutput, + didFinishProcessingPhoto photo: AVCapturePhoto, + error: Error?) { + guard let imageData = photo.fileDataRepresentation() else { + imageClosure?(nil, error) + return + } + + if let image = UIImage(data: imageData) { + imageClosure?(image, nil) + } + } +} diff --git a/IDNowTestTests/ImageViewModelTests.swift b/IDNowTestTests/ImageViewModelTests.swift new file mode 100644 index 0000000..750c183 --- /dev/null +++ b/IDNowTestTests/ImageViewModelTests.swift @@ -0,0 +1,178 @@ +// +// ImageViewModelTests.swift +// IDNowTestTests +// +// Created by Kristian Rusyn on 29/09/2024. +// + +import Foundation +import XCTest +import Combine +@testable import IDNowTest + +class ImageViewModelTests: XCTestCase { + + private var viewModel: ImageViewModel! + private var cancellables: Set! + + override func setUp() { + super.setUp() + cancellables = [] + + // Injecting a mock URLSession with no cache + let config = URLSessionConfiguration.default + config.protocolClasses = [MockURLProtocol.self] + config.requestCachePolicy = .reloadIgnoringLocalCacheData + let mockSession = URLSession(configuration: config) + + viewModel = ImageViewModel(image: UIImage(), session: mockSession) + } + + override func tearDown() { + // Clear ViewModel and Cancellables + viewModel = nil + cancellables = nil + + // Clear MockURLProtocol's stubbed data and error + MockURLProtocol.stubResponseData = nil + MockURLProtocol.stubResponseError = nil + + // Add a slight delay to ensure all asynchronous tasks are completed + let waitExpectation = XCTestExpectation(description: "Waiting for async clean-up") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + waitExpectation.fulfill() + } + wait(for: [waitExpectation], timeout: 0.5) + + super.tearDown() + } + + func testFetchProductSetsTitlePriceAndDescription() { + // Given + let product = Product(id: 1, title: "Test Product", description: "A sample product", price: 19.99) + let data = try! JSONEncoder().encode(product) + MockURLProtocol.stubResponseData = data + + // Individual expectations + let titleExpectation = XCTestExpectation(description: "Title updated") + let priceExpectation = XCTestExpectation(description: "Price updated") + let descriptionExpectation = XCTestExpectation(description: "Description updated") + + // Set up subscriptions before triggering the fetch + viewModel.$title + .dropFirst() + .sink { title in + print("Received title update in test: \(title)") + XCTAssertEqual(title, "Test Product") + titleExpectation.fulfill() + } + .store(in: &cancellables) + + viewModel.$price + .dropFirst() + .sink { price in + XCTAssertEqual(price, "$19.99") + priceExpectation.fulfill() + } + .store(in: &cancellables) + + viewModel.$description + .dropFirst() + .sink { description in + XCTAssertEqual(description, "A sample product") + descriptionExpectation.fulfill() + } + .store(in: &cancellables) + + // When + viewModel.fetchProduct() + + // Wait for all expectations + wait(for: [titleExpectation, priceExpectation, descriptionExpectation], timeout: 15.0) + } + + func testFetchProductSetsErrorMessageOnInvalidURL() { + // Given + MockURLProtocol.stubResponseError = URLError(.badURL) + + let expectation = XCTestExpectation(description: "Error message should be set") + + // Set up subscription before triggering the fetch + viewModel.$errorMessage + .dropFirst() + .sink { errorMessage in + XCTAssertNotNil(errorMessage) + XCTAssertEqual(errorMessage, URLError(.badURL).localizedDescription) + expectation.fulfill() + } + .store(in: &cancellables) + + // When + viewModel.fetchProduct() + + // Then + wait(for: [expectation], timeout: 5.0) + } + + func testIsLoadingStateDuringFetch() { + // Given + let product = Product(id: 1, title: "Test Product", description: "A sample product", price: 19.99) + let data = try! JSONEncoder().encode(product) + MockURLProtocol.stubResponseData = data + + let expectation1 = XCTestExpectation(description: "isLoading should be true when fetching starts") + let expectation2 = XCTestExpectation(description: "isLoading should be false when fetching ends") + + // Set up subscription before triggering the fetch + viewModel.$isLoading + .dropFirst() + .sink { isLoading in + if isLoading { + expectation1.fulfill() + } else { + expectation2.fulfill() + } + } + .store(in: &cancellables) + + // When + viewModel.fetchProduct() + + // Then + wait(for: [expectation1, expectation2], timeout: 5.0) + } +} + + +class MockURLProtocol: URLProtocol { + static var stubResponseData: Data? + static var stubResponseError: Error? + + override class func canInit(with request: URLRequest) -> Bool { + // This allows the mock to intercept all requests + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + if let error = MockURLProtocol.stubResponseError { + self.client?.urlProtocol(self, didFailWithError: error) + } else if let data = MockURLProtocol.stubResponseData { + let response = HTTPURLResponse(url: self.request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "application/json"])! + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + self.client?.urlProtocol(self, didLoad: data) + } + + self.client?.urlProtocolDidFinishLoading(self) + } + + override func stopLoading() { + // Required to override but no specific implementation needed + } +} From 44a9773c5587c772d848e26a3e6d3ffec8b730ef Mon Sep 17 00:00:00 2001 From: ChristRm Date: Mon, 30 Sep 2024 09:30:51 +0200 Subject: [PATCH 2/2] Update README.md --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ad7880..482bd75 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -# IDNowTest \ No newline at end of file +# Technical Test IDNow - iOS Camera & Image Preview Application + +## Overview +This project is an iOS application that allows users to capture an image using their device's camera, retrieve data from a JSON API, and display a preview of the captured image along with descriptive information from the API. The application follows the given requirements and aims to provide a simple yet effective demonstration of good software development practices, adhering to clean coding principles and a modular architecture. + +## Architecture & Technical Decisions +- **Architecture**: The application is mix of using the **MVVM (Model-View-ViewModel)** pattern and **MVC (Model-View-Controller)** pattern. **MVVM** was chosen for its clear separation of concerns, making the UI logic and business logic independent and easier to maintain. In addition **MVVM** provides the great way to test the logic of the particular screen. +- **Networking**: The networking layer is implemented using `URLSession` to make requests to the external API. +- **Combine Framework**: To manage asynchronous events, data bindings, and API responses, I used **Combine**, providing a reactive approach to state and data handling. Having reactive bindings is very practical when using MVVM pattern. +- **Camera Integration**: The `CameraService` component handles capturing images using `AVFoundation`. This keeps the view controller focused only on managing the UI. +- **Data Binding**: The view model is responsible for fetching data and updating the view. This design promotes testability and helps maintain a clear separation between business logic and UI. +- **UI Design**: The main screen is constructed in the Main.storyboard just for simplicity as it's a default XCode implementation when you create the project. The Image scren is constructed using with an `.xib` file. This is a better approach to avoid conflicts in .storyboard files, but also having the dedicated `.xib` file for each screen is more practical, takes less time to load and makes the UI construction easier to find. + +## Technical Choices & Best Practices +- **SOLID Principles**: Applied throughout the code to ensure better maintainability and extensibility. For example: + - **Single Responsibility**: The `CameraService` is solely responsible for handling camera interactions. + - **Dependency Inversion**: ImageViewModel depend on abstracted services, making it easy to mock or substitute during testing. The CameraViewController depends on `CameraService`, which is abstract type and can be injected from outside. +- **DRY (Don't Repeat Yourself)**: Code reuse is emphasized across view models and services, avoiding duplication. +- **KISS (Keep It Simple, Stupid)**: The application is kept as simple as possible to achieve the required functionality without unnecessary complexity. For instance, there is no need to have ViewModel for the camera screen, it is too simple. It's logic is purely of UI kind, streaming of the camera image and capturing the image. +There is no implementation of any sort of Router or Coordinator, as there just 2 screen, this would be and overkill. + +## Testing +- **Unit Tests**: `ImageViewModel` is covered with unit tests to verify their behavior. +- **Mocking**: The `MockURLProtocol` is used for testing network requests, ensuring consistent and controlled responses. + +## If Given More Time +- **Better UI/UX**: Currently, the UI/UX is as simple as possible, with more time I would think about better way to display the information in the Image screen. I would improve UI of the both Camera and Image screens. +- **More Robust Error Handling**: Currently, the error handling is minimal, mainly focused on user notifications for API errors and camera access issues. With more time, I would add finer-grained error handling and user-friendly messages for edge cases. +- **Code Coverage Expansion**: I would cover the CameraService with tests. + +## Task Breakdown +In a real work environment, I would break the task into smaller, reviewable sub-tasks as follows: +1. **Setup Project Structure and Dependencies**: Create a project, set up folders, and add necessary dependencies. +2. **Implement Camera Capture**: Develop the camera capture feature, ensuring the user can take pictures. +3. **API Integration**: Set up the network layer and integrate with the Dummy JSON API. +4. **Camera screen implementation**: Create the `.xib` file for the UI, ensuring all required elements are present. +5. **Image Preview with Data**: Combine camera functionality and API data to display the preview. +6. **Download Image Feature**: Implement functionality to save the captured image to the device. +7. **Testing**: Write unit tests for the view models and camera service. + +## Time Spent +The entire challenge was completed in approximately **6 hours**, which included: +- **Development**: 4.5 hours +- **Testing**: 1 hour +- **Documentation and Final Review**: 0.5 hours + +## Conclusion +This project demonstrates a simple yet structured approach to solving the given problem using modern iOS development techniques. The architectural decisions aim for maintainability, testability and simplicity.