diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..1aca155 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d4e0db --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.xcuserstate +*.xcworkspace +*.pbxproj +*.DS_Store \ No newline at end of file diff --git a/My App/My App.xcodeproj/project.pbxproj b/My App/My App.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e7afe38 --- /dev/null +++ b/My App/My App.xcodeproj/project.pbxproj @@ -0,0 +1,599 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + AB5C9B522ADBE02E00A358EC /* My_AppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5C9B512ADBE02E00A358EC /* My_AppApp.swift */; }; + AB5C9B542ADBE02E00A358EC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5C9B532ADBE02E00A358EC /* ContentView.swift */; }; + AB5C9B562ADBE02F00A358EC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB5C9B552ADBE02F00A358EC /* Assets.xcassets */; }; + AB5C9B592ADBE02F00A358EC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB5C9B582ADBE02F00A358EC /* Preview Assets.xcassets */; }; + AB5C9B632ADBE02F00A358EC /* My_AppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5C9B622ADBE02F00A358EC /* My_AppTests.swift */; }; + AB5C9B6D2ADBE02F00A358EC /* My_AppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5C9B6C2ADBE02F00A358EC /* My_AppUITests.swift */; }; + AB5C9B6F2ADBE02F00A358EC /* My_AppUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5C9B6E2ADBE02F00A358EC /* My_AppUITestsLaunchTests.swift */; }; + AB5C9B7C2ADBE8AF00A358EC /* ButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5C9B7B2ADBE8AF00A358EC /* ButtonView.swift */; }; + AB5C9B802ADBF38700A358EC /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5C9B7F2ADBF38700A358EC /* ListView.swift */; }; + AB5C9B822ADBF7B100A358EC /* NetworkingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5C9B812ADBF7B100A358EC /* NetworkingView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + AB5C9B5F2ADBE02F00A358EC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AB5C9B462ADBE02E00A358EC /* Project object */; + proxyType = 1; + remoteGlobalIDString = AB5C9B4D2ADBE02E00A358EC; + remoteInfo = "My App"; + }; + AB5C9B692ADBE02F00A358EC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AB5C9B462ADBE02E00A358EC /* Project object */; + proxyType = 1; + remoteGlobalIDString = AB5C9B4D2ADBE02E00A358EC; + remoteInfo = "My App"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + AB5C9B4E2ADBE02E00A358EC /* My App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "My App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + AB5C9B512ADBE02E00A358EC /* My_AppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = My_AppApp.swift; sourceTree = ""; }; + AB5C9B532ADBE02E00A358EC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + AB5C9B552ADBE02F00A358EC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AB5C9B582ADBE02F00A358EC /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + AB5C9B5E2ADBE02F00A358EC /* My AppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "My AppTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + AB5C9B622ADBE02F00A358EC /* My_AppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = My_AppTests.swift; sourceTree = ""; }; + AB5C9B682ADBE02F00A358EC /* My AppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "My AppUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + AB5C9B6C2ADBE02F00A358EC /* My_AppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = My_AppUITests.swift; sourceTree = ""; }; + AB5C9B6E2ADBE02F00A358EC /* My_AppUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = My_AppUITestsLaunchTests.swift; sourceTree = ""; }; + AB5C9B7B2ADBE8AF00A358EC /* ButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonView.swift; sourceTree = ""; }; + AB5C9B7F2ADBF38700A358EC /* ListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListView.swift; path = "My App/ListView.swift"; sourceTree = SOURCE_ROOT; }; + AB5C9B812ADBF7B100A358EC /* NetworkingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkingView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AB5C9B4B2ADBE02E00A358EC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AB5C9B5B2ADBE02F00A358EC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AB5C9B652ADBE02F00A358EC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + AB5C9B452ADBE02E00A358EC = { + isa = PBXGroup; + children = ( + AB5C9B502ADBE02E00A358EC /* My App */, + AB5C9B612ADBE02F00A358EC /* My AppTests */, + AB5C9B6B2ADBE02F00A358EC /* My AppUITests */, + AB5C9B4F2ADBE02E00A358EC /* Products */, + ); + sourceTree = ""; + }; + AB5C9B4F2ADBE02E00A358EC /* Products */ = { + isa = PBXGroup; + children = ( + AB5C9B4E2ADBE02E00A358EC /* My App.app */, + AB5C9B5E2ADBE02F00A358EC /* My AppTests.xctest */, + AB5C9B682ADBE02F00A358EC /* My AppUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + AB5C9B502ADBE02E00A358EC /* My App */ = { + isa = PBXGroup; + children = ( + AB5C9B512ADBE02E00A358EC /* My_AppApp.swift */, + AB5C9B532ADBE02E00A358EC /* ContentView.swift */, + AB5C9B552ADBE02F00A358EC /* Assets.xcassets */, + AB5C9B572ADBE02F00A358EC /* Preview Content */, + AB5C9B7B2ADBE8AF00A358EC /* ButtonView.swift */, + AB5C9B7F2ADBF38700A358EC /* ListView.swift */, + AB5C9B812ADBF7B100A358EC /* NetworkingView.swift */, + ); + path = "My App"; + sourceTree = ""; + }; + AB5C9B572ADBE02F00A358EC /* Preview Content */ = { + isa = PBXGroup; + children = ( + AB5C9B582ADBE02F00A358EC /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + AB5C9B612ADBE02F00A358EC /* My AppTests */ = { + isa = PBXGroup; + children = ( + AB5C9B622ADBE02F00A358EC /* My_AppTests.swift */, + ); + path = "My AppTests"; + sourceTree = ""; + }; + AB5C9B6B2ADBE02F00A358EC /* My AppUITests */ = { + isa = PBXGroup; + children = ( + AB5C9B6C2ADBE02F00A358EC /* My_AppUITests.swift */, + AB5C9B6E2ADBE02F00A358EC /* My_AppUITestsLaunchTests.swift */, + ); + path = "My AppUITests"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AB5C9B4D2ADBE02E00A358EC /* My App */ = { + isa = PBXNativeTarget; + buildConfigurationList = AB5C9B722ADBE02F00A358EC /* Build configuration list for PBXNativeTarget "My App" */; + buildPhases = ( + AB5C9B4A2ADBE02E00A358EC /* Sources */, + AB5C9B4B2ADBE02E00A358EC /* Frameworks */, + AB5C9B4C2ADBE02E00A358EC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "My App"; + productName = "My App"; + productReference = AB5C9B4E2ADBE02E00A358EC /* My App.app */; + productType = "com.apple.product-type.application"; + }; + AB5C9B5D2ADBE02F00A358EC /* My AppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = AB5C9B752ADBE02F00A358EC /* Build configuration list for PBXNativeTarget "My AppTests" */; + buildPhases = ( + AB5C9B5A2ADBE02F00A358EC /* Sources */, + AB5C9B5B2ADBE02F00A358EC /* Frameworks */, + AB5C9B5C2ADBE02F00A358EC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AB5C9B602ADBE02F00A358EC /* PBXTargetDependency */, + ); + name = "My AppTests"; + productName = "My AppTests"; + productReference = AB5C9B5E2ADBE02F00A358EC /* My AppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + AB5C9B672ADBE02F00A358EC /* My AppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = AB5C9B782ADBE02F00A358EC /* Build configuration list for PBXNativeTarget "My AppUITests" */; + buildPhases = ( + AB5C9B642ADBE02F00A358EC /* Sources */, + AB5C9B652ADBE02F00A358EC /* Frameworks */, + AB5C9B662ADBE02F00A358EC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AB5C9B6A2ADBE02F00A358EC /* PBXTargetDependency */, + ); + name = "My AppUITests"; + productName = "My AppUITests"; + productReference = AB5C9B682ADBE02F00A358EC /* My AppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AB5C9B462ADBE02E00A358EC /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + AB5C9B4D2ADBE02E00A358EC = { + CreatedOnToolsVersion = 15.0; + }; + AB5C9B5D2ADBE02F00A358EC = { + CreatedOnToolsVersion = 15.0; + TestTargetID = AB5C9B4D2ADBE02E00A358EC; + }; + AB5C9B672ADBE02F00A358EC = { + CreatedOnToolsVersion = 15.0; + TestTargetID = AB5C9B4D2ADBE02E00A358EC; + }; + }; + }; + buildConfigurationList = AB5C9B492ADBE02E00A358EC /* Build configuration list for PBXProject "My App" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AB5C9B452ADBE02E00A358EC; + productRefGroup = AB5C9B4F2ADBE02E00A358EC /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AB5C9B4D2ADBE02E00A358EC /* My App */, + AB5C9B5D2ADBE02F00A358EC /* My AppTests */, + AB5C9B672ADBE02F00A358EC /* My AppUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AB5C9B4C2ADBE02E00A358EC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AB5C9B592ADBE02F00A358EC /* Preview Assets.xcassets in Resources */, + AB5C9B562ADBE02F00A358EC /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AB5C9B5C2ADBE02F00A358EC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AB5C9B662ADBE02F00A358EC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AB5C9B4A2ADBE02E00A358EC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AB5C9B822ADBF7B100A358EC /* NetworkingView.swift in Sources */, + AB5C9B802ADBF38700A358EC /* ListView.swift in Sources */, + AB5C9B542ADBE02E00A358EC /* ContentView.swift in Sources */, + AB5C9B522ADBE02E00A358EC /* My_AppApp.swift in Sources */, + AB5C9B7C2ADBE8AF00A358EC /* ButtonView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AB5C9B5A2ADBE02F00A358EC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AB5C9B632ADBE02F00A358EC /* My_AppTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AB5C9B642ADBE02F00A358EC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AB5C9B6F2ADBE02F00A358EC /* My_AppUITestsLaunchTests.swift in Sources */, + AB5C9B6D2ADBE02F00A358EC /* My_AppUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AB5C9B602ADBE02F00A358EC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AB5C9B4D2ADBE02E00A358EC /* My App */; + targetProxy = AB5C9B5F2ADBE02F00A358EC /* PBXContainerItemProxy */; + }; + AB5C9B6A2ADBE02F00A358EC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AB5C9B4D2ADBE02E00A358EC /* My App */; + targetProxy = AB5C9B692ADBE02F00A358EC /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + AB5C9B702ADBE02F00A358EC /* 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.0; + 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; + }; + AB5C9B712ADBE02F00A358EC /* 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.0; + 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; + }; + AB5C9B732ADBE02F00A358EC /* 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_ASSET_PATHS = "\"My App/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + 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 = "com.irfan.My-App"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AB5C9B742ADBE02F00A358EC /* 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_ASSET_PATHS = "\"My App/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + 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 = "com.irfan.My-App"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + AB5C9B762ADBE02F00A358EC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.irfan.My-AppTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/My App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/My App"; + }; + name = Debug; + }; + AB5C9B772ADBE02F00A358EC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.irfan.My-AppTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/My App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/My App"; + }; + name = Release; + }; + AB5C9B792ADBE02F00A358EC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.irfan.My-AppUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "My App"; + }; + name = Debug; + }; + AB5C9B7A2ADBE02F00A358EC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.irfan.My-AppUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "My App"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AB5C9B492ADBE02E00A358EC /* Build configuration list for PBXProject "My App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AB5C9B702ADBE02F00A358EC /* Debug */, + AB5C9B712ADBE02F00A358EC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AB5C9B722ADBE02F00A358EC /* Build configuration list for PBXNativeTarget "My App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AB5C9B732ADBE02F00A358EC /* Debug */, + AB5C9B742ADBE02F00A358EC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AB5C9B752ADBE02F00A358EC /* Build configuration list for PBXNativeTarget "My AppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AB5C9B762ADBE02F00A358EC /* Debug */, + AB5C9B772ADBE02F00A358EC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AB5C9B782ADBE02F00A358EC /* Build configuration list for PBXNativeTarget "My AppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AB5C9B792ADBE02F00A358EC /* Debug */, + AB5C9B7A2ADBE02F00A358EC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AB5C9B462ADBE02E00A358EC /* Project object */; +} diff --git a/My App/My App.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/My App/My App.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/My App/My App.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/My App/My App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/My App/My App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/My App/My App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/My App/My App.xcodeproj/project.xcworkspace/xcuserdata/inafi.xcuserdatad/UserInterfaceState.xcuserstate b/My App/My App.xcodeproj/project.xcworkspace/xcuserdata/inafi.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..f1f2688 Binary files /dev/null and b/My App/My App.xcodeproj/project.xcworkspace/xcuserdata/inafi.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/My App/My App.xcodeproj/xcuserdata/inafi.xcuserdatad/xcschemes/xcschememanagement.plist b/My App/My App.xcodeproj/xcuserdata/inafi.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..bb26153 --- /dev/null +++ b/My App/My App.xcodeproj/xcuserdata/inafi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + My App.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/My App/My App/Assets.xcassets/AccentColor.colorset/Contents.json b/My App/My App/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/My App/My App/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/My App/My App/Assets.xcassets/AppIcon.appiconset/Contents.json b/My App/My App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/My App/My App/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/My App/My App/Assets.xcassets/Contents.json b/My App/My App/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/My App/My App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/My App/My App/ButtonView.swift b/My App/My App/ButtonView.swift new file mode 100644 index 0000000..6efef00 --- /dev/null +++ b/My App/My App/ButtonView.swift @@ -0,0 +1,20 @@ +// +// ButtonView.swift +// My App +// +// Created by Irfan Nafi on 10/15/23. +// + +import SwiftUI + +struct ButtonView: View { + var label: String + + var body: some View { + Button(action: { + print("Button tapped") + }, label: { + Text(label) + }) + } +} diff --git a/My App/My App/ContentView.swift b/My App/My App/ContentView.swift new file mode 100644 index 0000000..5ae2fb8 --- /dev/null +++ b/My App/My App/ContentView.swift @@ -0,0 +1,33 @@ +// +// ContentView.swift +// My App +// +// Created by Irfan Nafi on 10/15/23. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + NavigationView { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + ButtonView(label: "Tap me!") + NavigationLink(destination: ListView(items: ["Item 1", "Item 2", "Item 3"])) { + Text("Go to list") + } + NavigationLink(destination: NetworkingView()) { + Text("Go to networking") + } + } + .padding() + } + } +} + +#Preview { + ContentView() +} diff --git a/My App/My App/ListView.swift b/My App/My App/ListView.swift new file mode 100644 index 0000000..9716ebc --- /dev/null +++ b/My App/My App/ListView.swift @@ -0,0 +1,18 @@ +// +// ListView.swift +// My App +// +// Created by Irfan Nafi on 10/15/23. +// + +import SwiftUI + +struct ListView: View { + var items: [String] + + var body: some View { + List(items, id: \.self) { item in + Text(item) + } + } +} diff --git a/My App/My App/My_AppApp.swift b/My App/My App/My_AppApp.swift new file mode 100644 index 0000000..03cec91 --- /dev/null +++ b/My App/My App/My_AppApp.swift @@ -0,0 +1,17 @@ +// +// My_AppApp.swift +// My App +// +// Created by Irfan Nafi on 10/15/23. +// + +import SwiftUI + +@main +struct My_AppApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/My App/My App/NetworkingView.swift b/My App/My App/NetworkingView.swift new file mode 100644 index 0000000..26678b5 --- /dev/null +++ b/My App/My App/NetworkingView.swift @@ -0,0 +1,42 @@ +// +// NetworkingView.swift +// My App +// +// Created by Irfan Nafi on 10/15/23. +// + +import SwiftUI + +struct NetworkingView: View { + @State var posts: [Post] = [] + + var body: some View { + List(posts, id: \.id) { post in + VStack(alignment: .leading) { + Text(post.title) + .font(.headline) + Text(post.body) + .font(.subheadline) + } + } + .task { + do { + posts = try await fetchPosts() + } catch { + print(error) + } + } + } + + func fetchPosts() async throws -> [Post] { + let url = URL(string: "https://jsonplaceholder.typicode.com/posts")! + let (data, _) = try await URLSession.shared.data(from: url) + return try JSONDecoder().decode([Post].self, from: data) + } +} + +struct Post: Codable, Identifiable { + let id: Int + let title: String + let body: String +} diff --git a/My App/My App/Preview Content/Preview Assets.xcassets/Contents.json b/My App/My App/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/My App/My App/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/My App/My AppTests/My_AppTests.swift b/My App/My AppTests/My_AppTests.swift new file mode 100644 index 0000000..ebdffa0 --- /dev/null +++ b/My App/My AppTests/My_AppTests.swift @@ -0,0 +1,36 @@ +// +// My_AppTests.swift +// My AppTests +// +// Created by Irfan Nafi on 10/15/23. +// + +import XCTest +@testable import My_App + +final class My_AppTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/My App/My AppUITests/My_AppUITests.swift b/My App/My AppUITests/My_AppUITests.swift new file mode 100644 index 0000000..3d00d0b --- /dev/null +++ b/My App/My AppUITests/My_AppUITests.swift @@ -0,0 +1,41 @@ +// +// My_AppUITests.swift +// My AppUITests +// +// Created by Irfan Nafi on 10/15/23. +// + +import XCTest + +final class My_AppUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests itโ€™s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/My App/My AppUITests/My_AppUITestsLaunchTests.swift b/My App/My AppUITests/My_AppUITestsLaunchTests.swift new file mode 100644 index 0000000..64f063f --- /dev/null +++ b/My App/My AppUITests/My_AppUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// My_AppUITestsLaunchTests.swift +// My AppUITests +// +// Created by Irfan Nafi on 10/15/23. +// + +import XCTest + +final class My_AppUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/README.md b/README.md index fd7ec02..6e51316 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,281 @@ ### Overview +In this hackpack we will cover the basics of SwiftUI and create a simple iOS app. SwiftUI is a framework that allows you to build user interfaces across all Apple platforms with the power of Swift. It is a declarative framework, meaning that you can describe your user interface in a declarative manner, and SwiftUI will handle the rest. SwiftUI is also cross-platform, meaning that you can use the same code to build apps for iOS, macOS, watchOS, and tvOS (assuming the user interface is updated to fit the platform). Without further ado, let's get started! + +### Setup + +1. Install Xcode from the App Store (https://apps.apple.com/us/app/xcode/id497799835?mt=12) + +### Creating a New Project + +1. Open Xcode and click "Create New Project" + +![](/hackpack-assets/new_project_0.png) + +2. Select "App" under "iOS" + +![](/hackpack-assets/new_project_1.png) + +3. Enter a product name. For the organization identifier, enter your name in reverse domain name notation (e.g. com.apple). Leave the rest of the fields as default and click "Next" + +![](/hackpack-assets/new_project_2.png) + +4. Choose a location to save your project and click "Create". You should see a screen as shown below. On the right side of the screen, you should see a preview of your app. This is the default view that Xcode provides for you. You can click the play button on the top left of the screen to run your app in the simulator (or on your device if you have one connected). + +![](/hackpack-assets/new_project_3.png) + +### Overview of Xcode + +1. On the left side of the screen, you should see a file navigator. This is where you can view all of the files in your project and also create new files/folders. Above the folder tree, you should see a row of buttons that show you different properties of the project. For example, you can see all the breakpoints or monitor the memory usage of your app. + +![](/hackpack-assets/folder_tree.png) ![](/hackpack-assets/performance.png) + +2. At the very top, you should see the current device Xcode would run your app on. You can click this to change the device. + +![](/hackpack-assets/device.png) + +3. Github is integrated into Xcode. You can click the source control button on the top right of the screen to see all the changes you've made to your project. You can also commit and push your changes to Github from here. + +### Code Structure + +1. By default, you should see a file called `ContentView.swift`. This is the file that contains the code for the default view that Xcode provides for you. You can see the code for the view on the right side of the screen. + +2. `ContentView.swift` is called in `My_AppApp.swift`. This is the file that contains the code for the app itself. You can see the code for the app on the right side of the screen. For now, you can ignore this file. + +3. Let's go back to `ContentView.swift`. You'll notice that it's a struct that conforms to the `View` protocol. This means that it's a view. You can see the code for the view on the right side of the screen. The `body` property is a computed property that returns a `some View`. This means that the type of the view is unknown, but it conforms to the `View` protocol. The `some` keyword is used to hide the type of the view. You can read more about opaque return types [here](https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html). + +```swift +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} +``` + +4. Within the `body` property, you'll see a `VStack`. This is a view that stacks its children vertically. You can read more about it [here](https://developer.apple.com/documentation/swiftui/vstack). Within the `VStack`, you'll see an `Image` and a `Text`. These are both views. You can read more about `Image` [here](https://developer.apple.com/documentation/swiftui/image) and `Text` [here](https://developer.apple.com/documentation/swiftui/text). You'll also see a modifier called `padding`. This is a modifier that adds padding to the view. You can read more about it [here](). + +5. A modifier is a method that returns a modified version of the view. You can chain multiple modifiers together to modify the view. For example, you can add a background color to the view by adding a modifier called `background` to the end of the view. You can read more about it [here](). The order of the modifiers matters. If you add the `background` modifier before the `padding` modifier, the background color will only be applied to the view itself, not the padding. If you add the `background` modifier after the `padding` modifier, the background color will be applied to the view and the padding. + +```swift +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + .background(Color.red) + } +} +``` + +6. At the very bottom of the file, there's a `#Preview` struct (albeit with special syntax) that conforms to the `PreviewProvider` protocol. This struct is what creates the preview on the right side of the screen. + +### Creating a New View + +1. Let's add a new view to the project that creates a button. Right click on the folder tree and click "New File...". Select "Swift File" and click "Next". Enter "ButtonView.swift" as the file name and click "Create". You should see a new file called `ButtonView.swift` in the folder tree. + +2. In `ButtonView.swift`, add the following shown below. By creating a `var label: String` property, we can pass in a label to the view. You can read more about properties [here](https://docs.swift.org/swift-book/LanguageGuide/Properties.html). + +```swift +import SwiftUI + +struct ButtonView: View { + var label: String + + var body: some View { + Button(action: { + print("Button tapped") + }, label: { + Text(label) + }) + } +} +``` + +3. Go back to `ContentView.swift`. Add the following code to the `VStack`, under `Text`: + +```swift +ButtonView(label: "Tap me!") +``` + +4. Click the play button on the top left of the screen to run your app in the simulator. You should see a button that says "Tap me!". When you tap the button, you should see "Button tapped" printed in the console (which is at the bottom of the screen). + +![](/hackpack-assets/console.png) + +### Navigation + +1. Navigation is a way to navigate between different views. Let's add a navigation view to the app. Go back to `ContentView.swift`. Wrap the `VStack` in a `NavigationView`. You can read more about it [here](https://developer.apple.com/documentation/swiftui/navigationview). + +```swift +NavigationView { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + ButtonView(label: "Tap me!") + } + .padding() +} +``` + +2. Let's create a new screen that displays a list of items. Right click on the folder tree and click "New File...". Select "Swift File" and click "Next". Enter "ListView.swift" as the file name and click "Create". You should see a new file called `ListView.swift` in the folder tree. + +3. In `ListView.swift`, add the following shown below. By creating a `var items: [String]` property, we can pass in a list of items to the view. You can read more about arrays [here](https://docs.swift.org/swift-book/LanguageGuide/CollectionTypes.html#ID105). + +```swift +import SwiftUI + +struct ListView: View { + var items: [String] + + var body: some View { + List(items, id: \.self) { item in + Text(item) + } + } +} +``` + +4. Go back to `ContentView.swift`. Add a navigation link to the `VStack`, under `ButtonView`. You can read more about it [here](https://developer.apple.com/documentation/swiftui/navigationlink). + +```swift +NavigationView { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + ButtonView(label: "Tap me!") + NavigationLink(destination: ListView(items: ["Item 1", "Item 2", "Item 3"])) { + Text("Go to list") + } + } + .padding() +} +``` + +5. Here's what the app should look like now: + +![](/hackpack-assets/navigation.gif) + +### Networking + +1. Let's create a new screen that displays a list of items from an API. Right click on the folder tree and click "New File...". Select "Swift File" and click "Next". Enter "NetworkingView.swift" as the file name and click "Create". You should see a new file called `NetworkingView.swift` in the folder tree. + +2. In `NetworkingView.swift`, let's call the [JSON Placeholder API](https://jsonplaceholder.typicode.com/) to get a list of posts. Add the following shown below. By creating a `@State var posts: [Post]` property, we can store the list of posts in the view. You can read more about state [here](). + +```swift +import SwiftUI + +struct NetworkingView: View { + @State var posts: [Post] = [] + + var body: some View { + List(posts, id: \.id) { post in + VStack(alignment: .leading) { + Text(post.title) + .font(.headline) + Text(post.body) + .font(.subheadline) + } + } + .task { + do { + posts = try await fetchPosts() + } catch { + print(error) + } + } + } + + func fetchPosts() async throws -> [Post] { + let url = URL(string: "https://jsonplaceholder.typicode.com/posts")! + let (data, _) = try await URLSession.shared.data(from: url) + return try JSONDecoder().decode([Post].self, from: data) + } +} + +struct Post: Codable, Identifiable { + let id: Int + let title: String + let body: String +} +``` + +3. A task is a piece of code that runs asynchronously. You can read more about tasks [here](https://docs.swift.org/swift-book/LanguageGuide/Concurrency/ConcurrencyQuickTour.html#ID617). In the `task` modifier, we call the `fetchPosts` function to get a list of posts. We then set the `posts` property to the list of posts. You can read more about the `task` modifier [here](https://developer.apple.com/documentation/swiftui/view/task(id:priority:)). + +4. A `Codable` type is a type that can be encoded and decoded from JSON. You can read more about it [here](https://developer.apple.com/documentation/swift/codable). We create a `Post` struct that conforms to the `Codable` protocol. We also make it conform to the `Identifiable` protocol so that we can use it in a `List`. You can read more about the `Identifiable` protocol [here](https://developer.apple.com/documentation/swiftui/identifiable). + +5. An `Identifiable` type is a type that has a stable identity. You can read more about it [here](https://developer.apple.com/documentation/swiftui/identifiable). We make the `id` property of the `Post` struct the stable identity. + +6. Go back to `ContentView.swift`. Add a navigation link to the `VStack`, under `NavigationLink`. You can read more about it [here](https://developer.apple.com/documentation/swiftui/navigationlink). + +```swift +NavigationView { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + ButtonView(label: "Tap me!") + NavigationLink(destination: ListView(items: ["Item 1", "Item 2", "Item 3"])) { + Text("Go to list") + } + NavigationLink(destination: NetworkingView()) { + Text("Go to networking") + } + } + .padding() +} +``` + +7. Here's what the app should look like now: + +![](/hackpack-assets/network.gif) + +### Styling + +1. Now that we've gotten some basic SwiftUI down, let's style the app. Go back to `ContentView.swift`. Add the following code to the `VStack`, under `NavigationLink`. You can read more about the `background` modifier [here](). + +```swift +NavigationView { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + ButtonView(label: "Tap me!") + NavigationLink(destination: ListView(items: ["Item 1", "Item 2", "Item 3"])) { + Text("Go to list") + } + NavigationLink(destination: NetworkingView()) { + Text("Go to networking") + } + } + .padding() + .background(Color(.systemBackground)) +} +``` + # Congratulations! ๐ŸŽ‰ You've finished the SwiftUI hackpack! Now, you can build your own SwiftUI apps! diff --git a/hackpack-assets/console.png b/hackpack-assets/console.png new file mode 100644 index 0000000..aaf6c6e Binary files /dev/null and b/hackpack-assets/console.png differ diff --git a/hackpack-assets/device.png b/hackpack-assets/device.png new file mode 100644 index 0000000..c1909ec Binary files /dev/null and b/hackpack-assets/device.png differ diff --git a/hackpack-assets/folder_tree.png b/hackpack-assets/folder_tree.png new file mode 100644 index 0000000..5a194a3 Binary files /dev/null and b/hackpack-assets/folder_tree.png differ diff --git a/hackpack-assets/navigation.gif b/hackpack-assets/navigation.gif new file mode 100644 index 0000000..d8ee437 Binary files /dev/null and b/hackpack-assets/navigation.gif differ diff --git a/hackpack-assets/network.gif b/hackpack-assets/network.gif new file mode 100644 index 0000000..a9e531d Binary files /dev/null and b/hackpack-assets/network.gif differ diff --git a/hackpack-assets/new_project_0.png b/hackpack-assets/new_project_0.png new file mode 100644 index 0000000..41a216b Binary files /dev/null and b/hackpack-assets/new_project_0.png differ diff --git a/hackpack-assets/new_project_1.png b/hackpack-assets/new_project_1.png new file mode 100644 index 0000000..ab59005 Binary files /dev/null and b/hackpack-assets/new_project_1.png differ diff --git a/hackpack-assets/new_project_2.png b/hackpack-assets/new_project_2.png new file mode 100644 index 0000000..4c3148b Binary files /dev/null and b/hackpack-assets/new_project_2.png differ diff --git a/hackpack-assets/new_project_3.png b/hackpack-assets/new_project_3.png new file mode 100644 index 0000000..44dc0ee Binary files /dev/null and b/hackpack-assets/new_project_3.png differ diff --git a/hackpack-assets/performance.png b/hackpack-assets/performance.png new file mode 100644 index 0000000..451647c Binary files /dev/null and b/hackpack-assets/performance.png differ