├── .DS_Store ├── LVNTutorialApp ├── .gitignore ├── LVNTutorialApp.xcodeproj │ └── project.pbxproj └── LVNTutorialApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── CatRatingView.swift │ ├── ContentView.swift │ ├── LVNTutorialAppApp.swift │ ├── MyLoadingView.swift │ ├── MyRegistry.swift │ ├── NavFavoriteModifier.swift │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── README.md └── lvn_tutorial_backend ├── .DS_Store ├── .formatter.exs ├── .gitignore ├── README.md ├── assets ├── .DS_Store ├── js │ └── app.js └── vendor │ └── topbar.js ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── runtime.exs └── test.exs ├── lib ├── lvn_tutorial.ex ├── lvn_tutorial │ ├── application.ex │ └── favorites_store.ex ├── lvn_tutorial_web.ex └── lvn_tutorial_web │ ├── components │ ├── core_components.ex │ ├── layouts.ex │ └── layouts │ │ ├── app.html.heex │ │ └── root.html.heex │ ├── controllers │ ├── error_html.ex │ ├── error_json.ex │ ├── page_controller.ex │ ├── page_html.ex │ └── page_html │ │ └── home.html.heex │ ├── endpoint.ex │ ├── live │ ├── cat_live.ex │ └── cats_list_live.ex │ ├── modifiers │ └── nav_favorite.ex │ ├── router.ex │ └── telemetry.ex ├── mix.exs ├── mix.lock ├── priv └── static │ ├── favicon.ico │ ├── images │ └── logo.svg │ └── robots.txt └── test ├── lvn_tutorial_web └── controllers │ ├── error_html_test.exs │ ├── error_json_test.exs │ └── page_controller_test.exs ├── support └── conn_case.ex └── test_helper.exs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liveview-native/ios-tutorial/98a6943e25a405506bf483e24742425465b99083/.DS_Store -------------------------------------------------------------------------------- /LVNTutorialApp/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | ### Swift ### 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | .build/ 44 | 45 | # CocoaPods - Refactored to standalone file 46 | 47 | # Carthage - Refactored to standalone file 48 | 49 | # fastlane 50 | # 51 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 52 | # screenshots whenever they are needed. 53 | # For more information about the recommended setup visit: 54 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 55 | 56 | fastlane/report.xml 57 | fastlane/Preview.html 58 | fastlane/screenshots 59 | fastlane/test_output 60 | 61 | ### Xcode ### 62 | # Xcode 63 | # 64 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 65 | 66 | ## Build generated 67 | 68 | ## Various settings 69 | 70 | ## Other 71 | 72 | ### Xcode Patch ### 73 | *.xcodeproj/* 74 | !*.xcodeproj/project.pbxproj 75 | !*.xcodeproj/xcshareddata/ 76 | !*.xcworkspace/contents.xcworkspacedata 77 | /*.gcno 78 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 001CB8B029CA4EA6007C0F0F /* LiveViewNative in Frameworks */ = {isa = PBXBuildFile; productRef = 001CB8AF29CA4EA6007C0F0F /* LiveViewNative */; }; 11 | D61A45D528DBA5D3002BE511 /* LVNTutorialAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45D428DBA5D3002BE511 /* LVNTutorialAppApp.swift */; }; 12 | D61A45D728DBA5D3002BE511 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45D628DBA5D3002BE511 /* ContentView.swift */; }; 13 | D61A45D928DBA5D4002BE511 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D61A45D828DBA5D4002BE511 /* Assets.xcassets */; }; 14 | D61A45DC28DBA5D4002BE511 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D61A45DB28DBA5D4002BE511 /* Preview Assets.xcassets */; }; 15 | D665963E28E77DB900FD477F /* MyRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D665963D28E77DB900FD477F /* MyRegistry.swift */; }; 16 | D665964028E77DC200FD477F /* NavFavoriteModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D665963F28E77DC200FD477F /* NavFavoriteModifier.swift */; }; 17 | D665964228E77DCB00FD477F /* CatRatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D665964128E77DCB00FD477F /* CatRatingView.swift */; }; 18 | D665964428E77DD400FD477F /* MyLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D665964328E77DD400FD477F /* MyLoadingView.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | D61A45D128DBA5D3002BE511 /* LVNTutorialApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LVNTutorialApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | D61A45D428DBA5D3002BE511 /* LVNTutorialAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LVNTutorialAppApp.swift; sourceTree = ""; }; 24 | D61A45D628DBA5D3002BE511 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 25 | D61A45D828DBA5D4002BE511 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | D61A45DB28DBA5D4002BE511 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 27 | D665963D28E77DB900FD477F /* MyRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyRegistry.swift; sourceTree = ""; }; 28 | D665963F28E77DC200FD477F /* NavFavoriteModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavFavoriteModifier.swift; sourceTree = ""; }; 29 | D665964128E77DCB00FD477F /* CatRatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatRatingView.swift; sourceTree = ""; }; 30 | D665964328E77DD400FD477F /* MyLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyLoadingView.swift; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | D61A45CE28DBA5D3002BE511 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | 001CB8B029CA4EA6007C0F0F /* LiveViewNative in Frameworks */, 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | D61A45C828DBA5D3002BE511 = { 46 | isa = PBXGroup; 47 | children = ( 48 | D61A45D328DBA5D3002BE511 /* LVNTutorialApp */, 49 | D61A45D228DBA5D3002BE511 /* Products */, 50 | ); 51 | sourceTree = ""; 52 | }; 53 | D61A45D228DBA5D3002BE511 /* Products */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | D61A45D128DBA5D3002BE511 /* LVNTutorialApp.app */, 57 | ); 58 | name = Products; 59 | sourceTree = ""; 60 | }; 61 | D61A45D328DBA5D3002BE511 /* LVNTutorialApp */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | D61A45D428DBA5D3002BE511 /* LVNTutorialAppApp.swift */, 65 | D61A45D628DBA5D3002BE511 /* ContentView.swift */, 66 | D665963D28E77DB900FD477F /* MyRegistry.swift */, 67 | D665963F28E77DC200FD477F /* NavFavoriteModifier.swift */, 68 | D665964128E77DCB00FD477F /* CatRatingView.swift */, 69 | D665964328E77DD400FD477F /* MyLoadingView.swift */, 70 | D61A45D828DBA5D4002BE511 /* Assets.xcassets */, 71 | D61A45DA28DBA5D4002BE511 /* Preview Content */, 72 | ); 73 | path = LVNTutorialApp; 74 | sourceTree = ""; 75 | }; 76 | D61A45DA28DBA5D4002BE511 /* Preview Content */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | D61A45DB28DBA5D4002BE511 /* Preview Assets.xcassets */, 80 | ); 81 | path = "Preview Content"; 82 | sourceTree = ""; 83 | }; 84 | /* End PBXGroup section */ 85 | 86 | /* Begin PBXNativeTarget section */ 87 | D61A45D028DBA5D3002BE511 /* LVNTutorialApp */ = { 88 | isa = PBXNativeTarget; 89 | buildConfigurationList = D61A45DF28DBA5D4002BE511 /* Build configuration list for PBXNativeTarget "LVNTutorialApp" */; 90 | buildPhases = ( 91 | D61A45CD28DBA5D3002BE511 /* Sources */, 92 | D61A45CE28DBA5D3002BE511 /* Frameworks */, 93 | D61A45CF28DBA5D3002BE511 /* Resources */, 94 | ); 95 | buildRules = ( 96 | ); 97 | dependencies = ( 98 | ); 99 | name = LVNTutorialApp; 100 | packageProductDependencies = ( 101 | 001CB8AF29CA4EA6007C0F0F /* LiveViewNative */, 102 | ); 103 | productName = LVNTutorialApp; 104 | productReference = D61A45D128DBA5D3002BE511 /* LVNTutorialApp.app */; 105 | productType = "com.apple.product-type.application"; 106 | }; 107 | /* End PBXNativeTarget section */ 108 | 109 | /* Begin PBXProject section */ 110 | D61A45C928DBA5D3002BE511 /* Project object */ = { 111 | isa = PBXProject; 112 | attributes = { 113 | BuildIndependentTargetsInParallel = 1; 114 | LastSwiftUpdateCheck = 1400; 115 | LastUpgradeCheck = 1400; 116 | TargetAttributes = { 117 | D61A45D028DBA5D3002BE511 = { 118 | CreatedOnToolsVersion = 14.0; 119 | }; 120 | }; 121 | }; 122 | buildConfigurationList = D61A45CC28DBA5D3002BE511 /* Build configuration list for PBXProject "LVNTutorialApp" */; 123 | compatibilityVersion = "Xcode 14.0"; 124 | developmentRegion = en; 125 | hasScannedForEncodings = 0; 126 | knownRegions = ( 127 | en, 128 | Base, 129 | ); 130 | mainGroup = D61A45C828DBA5D3002BE511; 131 | packageReferences = ( 132 | 001CB8AE29CA4EA6007C0F0F /* XCRemoteSwiftPackageReference "liveview-client-swiftui" */, 133 | ); 134 | productRefGroup = D61A45D228DBA5D3002BE511 /* Products */; 135 | projectDirPath = ""; 136 | projectRoot = ""; 137 | targets = ( 138 | D61A45D028DBA5D3002BE511 /* LVNTutorialApp */, 139 | ); 140 | }; 141 | /* End PBXProject section */ 142 | 143 | /* Begin PBXResourcesBuildPhase section */ 144 | D61A45CF28DBA5D3002BE511 /* Resources */ = { 145 | isa = PBXResourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | D61A45DC28DBA5D4002BE511 /* Preview Assets.xcassets in Resources */, 149 | D61A45D928DBA5D4002BE511 /* Assets.xcassets in Resources */, 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXResourcesBuildPhase section */ 154 | 155 | /* Begin PBXSourcesBuildPhase section */ 156 | D61A45CD28DBA5D3002BE511 /* Sources */ = { 157 | isa = PBXSourcesBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | D665964028E77DC200FD477F /* NavFavoriteModifier.swift in Sources */, 161 | D61A45D728DBA5D3002BE511 /* ContentView.swift in Sources */, 162 | D61A45D528DBA5D3002BE511 /* LVNTutorialAppApp.swift in Sources */, 163 | D665964428E77DD400FD477F /* MyLoadingView.swift in Sources */, 164 | D665964228E77DCB00FD477F /* CatRatingView.swift in Sources */, 165 | D665963E28E77DB900FD477F /* MyRegistry.swift in Sources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXSourcesBuildPhase section */ 170 | 171 | /* Begin XCBuildConfiguration section */ 172 | D61A45DD28DBA5D4002BE511 /* Debug */ = { 173 | isa = XCBuildConfiguration; 174 | buildSettings = { 175 | ALWAYS_SEARCH_USER_PATHS = NO; 176 | CLANG_ANALYZER_NONNULL = YES; 177 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 179 | CLANG_ENABLE_MODULES = YES; 180 | CLANG_ENABLE_OBJC_ARC = YES; 181 | CLANG_ENABLE_OBJC_WEAK = YES; 182 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 183 | CLANG_WARN_BOOL_CONVERSION = YES; 184 | CLANG_WARN_COMMA = YES; 185 | CLANG_WARN_CONSTANT_CONVERSION = YES; 186 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 187 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 188 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 189 | CLANG_WARN_EMPTY_BODY = YES; 190 | CLANG_WARN_ENUM_CONVERSION = YES; 191 | CLANG_WARN_INFINITE_RECURSION = YES; 192 | CLANG_WARN_INT_CONVERSION = YES; 193 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 195 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 196 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 197 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 198 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 199 | CLANG_WARN_STRICT_PROTOTYPES = YES; 200 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 201 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 202 | CLANG_WARN_UNREACHABLE_CODE = YES; 203 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 204 | COPY_PHASE_STRIP = NO; 205 | DEBUG_INFORMATION_FORMAT = dwarf; 206 | ENABLE_STRICT_OBJC_MSGSEND = YES; 207 | ENABLE_TESTABILITY = YES; 208 | GCC_C_LANGUAGE_STANDARD = gnu11; 209 | GCC_DYNAMIC_NO_PIC = NO; 210 | GCC_NO_COMMON_BLOCKS = YES; 211 | GCC_OPTIMIZATION_LEVEL = 0; 212 | GCC_PREPROCESSOR_DEFINITIONS = ( 213 | "DEBUG=1", 214 | "$(inherited)", 215 | ); 216 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 217 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 218 | GCC_WARN_UNDECLARED_SELECTOR = YES; 219 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 220 | GCC_WARN_UNUSED_FUNCTION = YES; 221 | GCC_WARN_UNUSED_VARIABLE = YES; 222 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 223 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 224 | MTL_FAST_MATH = YES; 225 | ONLY_ACTIVE_ARCH = YES; 226 | SDKROOT = iphoneos; 227 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 228 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 229 | }; 230 | name = Debug; 231 | }; 232 | D61A45DE28DBA5D4002BE511 /* Release */ = { 233 | isa = XCBuildConfiguration; 234 | buildSettings = { 235 | ALWAYS_SEARCH_USER_PATHS = NO; 236 | CLANG_ANALYZER_NONNULL = YES; 237 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 238 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 239 | CLANG_ENABLE_MODULES = YES; 240 | CLANG_ENABLE_OBJC_ARC = YES; 241 | CLANG_ENABLE_OBJC_WEAK = YES; 242 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 243 | CLANG_WARN_BOOL_CONVERSION = YES; 244 | CLANG_WARN_COMMA = YES; 245 | CLANG_WARN_CONSTANT_CONVERSION = YES; 246 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 247 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 248 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 258 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 259 | CLANG_WARN_STRICT_PROTOTYPES = YES; 260 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 261 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 262 | CLANG_WARN_UNREACHABLE_CODE = YES; 263 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 264 | COPY_PHASE_STRIP = NO; 265 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 266 | ENABLE_NS_ASSERTIONS = NO; 267 | ENABLE_STRICT_OBJC_MSGSEND = YES; 268 | GCC_C_LANGUAGE_STANDARD = gnu11; 269 | GCC_NO_COMMON_BLOCKS = YES; 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 277 | MTL_ENABLE_DEBUG_INFO = NO; 278 | MTL_FAST_MATH = YES; 279 | SDKROOT = iphoneos; 280 | SWIFT_COMPILATION_MODE = wholemodule; 281 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 282 | VALIDATE_PRODUCT = YES; 283 | }; 284 | name = Release; 285 | }; 286 | D61A45E028DBA5D4002BE511 /* Debug */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 290 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 291 | CODE_SIGN_STYLE = Automatic; 292 | CURRENT_PROJECT_VERSION = 1; 293 | DEVELOPMENT_ASSET_PATHS = "\"LVNTutorialApp/Preview Content\""; 294 | DEVELOPMENT_TEAM = ""; 295 | ENABLE_PREVIEWS = YES; 296 | GENERATE_INFOPLIST_FILE = YES; 297 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 298 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 299 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 300 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 301 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 302 | LD_RUNPATH_SEARCH_PATHS = ( 303 | "$(inherited)", 304 | "@executable_path/Frameworks", 305 | ); 306 | MARKETING_VERSION = 1.0; 307 | PRODUCT_BUNDLE_IDENTIFIER = com.example.LVNTutorialApp; 308 | PRODUCT_NAME = "$(TARGET_NAME)"; 309 | SWIFT_EMIT_LOC_STRINGS = YES; 310 | SWIFT_VERSION = 5.0; 311 | TARGETED_DEVICE_FAMILY = "1,2"; 312 | }; 313 | name = Debug; 314 | }; 315 | D61A45E128DBA5D4002BE511 /* Release */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 319 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 320 | CODE_SIGN_STYLE = Automatic; 321 | CURRENT_PROJECT_VERSION = 1; 322 | DEVELOPMENT_ASSET_PATHS = "\"LVNTutorialApp/Preview Content\""; 323 | DEVELOPMENT_TEAM = ""; 324 | ENABLE_PREVIEWS = YES; 325 | GENERATE_INFOPLIST_FILE = YES; 326 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 327 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 328 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 329 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 330 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 331 | LD_RUNPATH_SEARCH_PATHS = ( 332 | "$(inherited)", 333 | "@executable_path/Frameworks", 334 | ); 335 | MARKETING_VERSION = 1.0; 336 | PRODUCT_BUNDLE_IDENTIFIER = com.example.LVNTutorialApp; 337 | PRODUCT_NAME = "$(TARGET_NAME)"; 338 | SWIFT_EMIT_LOC_STRINGS = YES; 339 | SWIFT_VERSION = 5.0; 340 | TARGETED_DEVICE_FAMILY = "1,2"; 341 | }; 342 | name = Release; 343 | }; 344 | /* End XCBuildConfiguration section */ 345 | 346 | /* Begin XCConfigurationList section */ 347 | D61A45CC28DBA5D3002BE511 /* Build configuration list for PBXProject "LVNTutorialApp" */ = { 348 | isa = XCConfigurationList; 349 | buildConfigurations = ( 350 | D61A45DD28DBA5D4002BE511 /* Debug */, 351 | D61A45DE28DBA5D4002BE511 /* Release */, 352 | ); 353 | defaultConfigurationIsVisible = 0; 354 | defaultConfigurationName = Release; 355 | }; 356 | D61A45DF28DBA5D4002BE511 /* Build configuration list for PBXNativeTarget "LVNTutorialApp" */ = { 357 | isa = XCConfigurationList; 358 | buildConfigurations = ( 359 | D61A45E028DBA5D4002BE511 /* Debug */, 360 | D61A45E128DBA5D4002BE511 /* Release */, 361 | ); 362 | defaultConfigurationIsVisible = 0; 363 | defaultConfigurationName = Release; 364 | }; 365 | /* End XCConfigurationList section */ 366 | 367 | /* Begin XCRemoteSwiftPackageReference section */ 368 | 001CB8AE29CA4EA6007C0F0F /* XCRemoteSwiftPackageReference "liveview-client-swiftui" */ = { 369 | isa = XCRemoteSwiftPackageReference; 370 | repositoryURL = "https://github.com/liveview-native/liveview-client-swiftui"; 371 | requirement = { 372 | branch = main; 373 | kind = branch; 374 | }; 375 | }; 376 | /* End XCRemoteSwiftPackageReference section */ 377 | 378 | /* Begin XCSwiftPackageProductDependency section */ 379 | 001CB8AF29CA4EA6007C0F0F /* LiveViewNative */ = { 380 | isa = XCSwiftPackageProductDependency; 381 | package = 001CB8AE29CA4EA6007C0F0F /* XCRemoteSwiftPackageReference "liveview-client-swiftui" */; 382 | productName = LiveViewNative; 383 | }; 384 | /* End XCSwiftPackageProductDependency section */ 385 | }; 386 | rootObject = D61A45C928DBA5D3002BE511 /* Project object */; 387 | } 388 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp/CatRatingView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import LiveViewNative 3 | 4 | struct CatRatingView: View { 5 | @Attribute("score") private var score: Int 6 | @LiveContext private var context 7 | @State var editedScore: Int? 8 | @State var width: CGFloat = 0 9 | 10 | var effectiveScore: Int { 11 | editedScore ?? score 12 | } 13 | 14 | var body: some View { 15 | HStack(spacing: 4) { 16 | ForEach(0.. Int { 48 | let fraction = max(0, min(1, point.x / width)) 49 | return Int(ceil(fraction * 5)) 50 | } 51 | 52 | func setScore(_ score: Int) { 53 | Task { 54 | editedScore = score 55 | try? await context.coordinator.pushEvent(type: "click", event: "change-score", value: score) 56 | editedScore = nil 57 | } 58 | } 59 | 60 | struct WidthPrefKey: PreferenceKey { 61 | static var defaultValue: CGFloat = 0 62 | 63 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { 64 | value = nextValue() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import LiveViewNative 3 | 4 | @MainActor 5 | struct ContentView: View { 6 | @StateObject private var session: LiveSessionCoordinator = { 7 | var config = LiveSessionConfiguration() 8 | config.navigationMode = .enabled 9 | return LiveSessionCoordinator(URL(string: "http://localhost:4000/cats")!, config: config) 10 | }() 11 | 12 | var body: some View { 13 | LiveView(session: session) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp/LVNTutorialAppApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LVNTutorialAppApp.swift 3 | // LVNTutorialApp 4 | // 5 | // Created by Shadowfacts on 9/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct LVNTutorialAppApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp/MyLoadingView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MyLoadingView: View { 4 | @State var isAnimating = false 5 | 6 | var body: some View { 7 | ZStack { 8 | ForEach(0..<8) { i in 9 | let angle = Double(i)/8 * 2 * .pi 10 | Text(i % 2 == 0 ? "🐈" : "🐈‍⬛") 11 | .rotationEffect(.radians(angle + 0.5 * .pi)) 12 | .offset(x: 50 * cos(angle), y: 50 * sin(angle)) 13 | } 14 | } 15 | .rotationEffect(.degrees(isAnimating ? 0 : 360), anchor: UnitPoint(x: 0.5, y: 0.5)) 16 | .onAppear { 17 | withAnimation(.linear(duration: 3).repeatForever(autoreverses: false)) { 18 | isAnimating = true 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp/MyRegistry.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import LiveViewNative 3 | 4 | struct MyRegistry: RootRegistry { 5 | enum TagName: String { 6 | case catRating = "CatRating" 7 | } 8 | enum ModifierType: String { 9 | case navFavorite = "nav_favorite" 10 | } 11 | 12 | static func lookup(_ name: TagName, element: ElementNode) -> some View { 13 | switch name { 14 | case .catRating: 15 | CatRatingView() 16 | } 17 | } 18 | 19 | static func decodeModifier(_ type: ModifierType, from decoder: Decoder) throws -> some ViewModifier { 20 | switch type { 21 | case .navFavorite: 22 | try NavFavoriteModifier(from: decoder) 23 | } 24 | } 25 | 26 | static func loadingView(for url: URL, state: LiveSessionState) -> some View { 27 | if case .connectionFailed(let error) = state { 28 | VStack { 29 | Text("⚠️😿") 30 | .font(.largeTitle) 31 | Text(error.localizedDescription) 32 | } 33 | } else { 34 | MyLoadingView() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp/NavFavoriteModifier.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import LiveViewNative 3 | 4 | struct NavFavoriteModifier: ViewModifier, Decodable { 5 | let isFavorite: Bool 6 | @LiveContext private var context 7 | 8 | init(from decoder: Decoder) throws { 9 | let container = try decoder.container(keyedBy: CodingKeys.self) 10 | self.isFavorite = try container.decode(Bool.self, forKey: .isFavorite) 11 | } 12 | 13 | func body(content: Content) -> some View { 14 | content 15 | .toolbar { 16 | ToolbarItem(placement: .navigationBarTrailing) { 17 | Button { 18 | Task { 19 | try? await context.coordinator.pushEvent(type: "click", event: "toggle-favorite", value: [String:Any]()) 20 | } 21 | } label: { 22 | Image(systemName: isFavorite ? "star.fill" : "star") 23 | } 24 | } 25 | } 26 | } 27 | 28 | enum CodingKeys: String, CodingKey { 29 | case isFavorite 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LVNTutorialApp/LVNTutorialApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ios-tutorial 2 | 3 | ## WARNING 4 | 5 | > This tutorial is quite old and we can work on updating it but in the near future we will be moving over to a Livebook version of the tutorial. In the meantime if you have anything that doesn't work or is confusing please open an issue. 6 | 7 | This is the sample project for [liveview-client-swiftui](https://github.com/liveview-native/liveview-client-swiftui) that the [tutorial](https://liveview-native.github.io/liveview-client-swiftui/tutorials/yourfirstapp) walks you through building. Browse the commits in this repo to see the state of the project at the end of each section of the tutorial. 8 | 9 | Contributions are not accepted in this repo. If you wish to suggest changes to the tutorial, please do so on the main liveview-client-swiftui repo. 10 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liveview-native/ios-tutorial/98a6943e25a405506bf483e24742425465b99083/lvn_tutorial_backend/.DS_Store -------------------------------------------------------------------------------- /lvn_tutorial_backend/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | plugins: [Phoenix.LiveView.HTMLFormatter], 4 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | lvn_tutorial-*.tar 27 | 28 | # Ignore assets that are produced by build tools. 29 | /priv/static/assets/ 30 | 31 | # Ignore digested assets cache. 32 | /priv/static/cache_manifest.json 33 | 34 | # In case you use Node.js/npm, you want to ignore these. 35 | npm-debug.log 36 | /assets/node_modules/ 37 | 38 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/README.md: -------------------------------------------------------------------------------- 1 | # LvnTutorial 2 | 3 | To start your Phoenix server: 4 | 5 | * Run `mix setup` to install and setup dependencies 6 | * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` 7 | 8 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 9 | 10 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 11 | 12 | ## Learn more 13 | 14 | * Official website: https://www.phoenixframework.org/ 15 | * Guides: https://hexdocs.pm/phoenix/overview.html 16 | * Docs: https://hexdocs.pm/phoenix 17 | * Forum: https://elixirforum.com/c/phoenix-forum 18 | * Source: https://github.com/phoenixframework/phoenix 19 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liveview-native/ios-tutorial/98a6943e25a405506bf483e24742425465b99083/lvn_tutorial_backend/assets/.DS_Store -------------------------------------------------------------------------------- /lvn_tutorial_backend/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 2 | // to get started and then uncomment the line below. 3 | // import "./user_socket.js" 4 | 5 | // You can include dependencies in two ways. 6 | // 7 | // The simplest option is to put them in assets/vendor and 8 | // import them using relative paths: 9 | // 10 | // import "../vendor/some-package.js" 11 | // 12 | // Alternatively, you can `npm install some-package --prefix assets` and import 13 | // them using a path starting with the package name: 14 | // 15 | // import "some-package" 16 | // 17 | 18 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 19 | import "phoenix_html" 20 | // Establish Phoenix Socket and LiveView configuration. 21 | import {Socket} from "phoenix" 22 | import {LiveSocket} from "phoenix_live_view" 23 | import topbar from "../vendor/topbar" 24 | 25 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 26 | let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) 27 | 28 | // Show progress bar on live navigation and form submits 29 | topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 30 | window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) 31 | window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) 32 | 33 | // connect if there are any LiveViews on the page 34 | liveSocket.connect() 35 | 36 | // expose liveSocket on window for web console debug logs and latency simulation: 37 | // >> liveSocket.enableDebug() 38 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 39 | // >> liveSocket.disableLatencySim() 40 | window.liveSocket = liveSocket 41 | 42 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 2.0.0, 2023-02-04 4 | * https://buunguyen.github.io/topbar 5 | * Copyright (c) 2021 Buu Nguyen 6 | */ 7 | (function (window, document) { 8 | "use strict"; 9 | 10 | // https://gist.github.com/paulirish/1579671 11 | (function () { 12 | var lastTime = 0; 13 | var vendors = ["ms", "moz", "webkit", "o"]; 14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 15 | window.requestAnimationFrame = 16 | window[vendors[x] + "RequestAnimationFrame"]; 17 | window.cancelAnimationFrame = 18 | window[vendors[x] + "CancelAnimationFrame"] || 19 | window[vendors[x] + "CancelRequestAnimationFrame"]; 20 | } 21 | if (!window.requestAnimationFrame) 22 | window.requestAnimationFrame = function (callback, element) { 23 | var currTime = new Date().getTime(); 24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 25 | var id = window.setTimeout(function () { 26 | callback(currTime + timeToCall); 27 | }, timeToCall); 28 | lastTime = currTime + timeToCall; 29 | return id; 30 | }; 31 | if (!window.cancelAnimationFrame) 32 | window.cancelAnimationFrame = function (id) { 33 | clearTimeout(id); 34 | }; 35 | })(); 36 | 37 | var canvas, 38 | currentProgress, 39 | showing, 40 | progressTimerId = null, 41 | fadeTimerId = null, 42 | delayTimerId = null, 43 | addEvent = function (elem, type, handler) { 44 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 46 | else elem["on" + type] = handler; 47 | }, 48 | options = { 49 | autoRun: true, 50 | barThickness: 3, 51 | barColors: { 52 | 0: "rgba(26, 188, 156, .9)", 53 | ".25": "rgba(52, 152, 219, .9)", 54 | ".50": "rgba(241, 196, 15, .9)", 55 | ".75": "rgba(230, 126, 34, .9)", 56 | "1.0": "rgba(211, 84, 0, .9)", 57 | }, 58 | shadowBlur: 10, 59 | shadowColor: "rgba(0, 0, 0, .6)", 60 | className: null, 61 | }, 62 | repaint = function () { 63 | canvas.width = window.innerWidth; 64 | canvas.height = options.barThickness * 5; // need space for shadow 65 | 66 | var ctx = canvas.getContext("2d"); 67 | ctx.shadowBlur = options.shadowBlur; 68 | ctx.shadowColor = options.shadowColor; 69 | 70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 71 | for (var stop in options.barColors) 72 | lineGradient.addColorStop(stop, options.barColors[stop]); 73 | ctx.lineWidth = options.barThickness; 74 | ctx.beginPath(); 75 | ctx.moveTo(0, options.barThickness / 2); 76 | ctx.lineTo( 77 | Math.ceil(currentProgress * canvas.width), 78 | options.barThickness / 2 79 | ); 80 | ctx.strokeStyle = lineGradient; 81 | ctx.stroke(); 82 | }, 83 | createCanvas = function () { 84 | canvas = document.createElement("canvas"); 85 | var style = canvas.style; 86 | style.position = "fixed"; 87 | style.top = style.left = style.right = style.margin = style.padding = 0; 88 | style.zIndex = 100001; 89 | style.display = "none"; 90 | if (options.className) canvas.classList.add(options.className); 91 | document.body.appendChild(canvas); 92 | addEvent(window, "resize", repaint); 93 | }, 94 | topbar = { 95 | config: function (opts) { 96 | for (var key in opts) 97 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 98 | }, 99 | show: function (delay) { 100 | if (showing) return; 101 | if (delay) { 102 | if (delayTimerId) return; 103 | delayTimerId = setTimeout(() => topbar.show(), delay); 104 | } else { 105 | showing = true; 106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 107 | if (!canvas) createCanvas(); 108 | canvas.style.opacity = 1; 109 | canvas.style.display = "block"; 110 | topbar.progress(0); 111 | if (options.autoRun) { 112 | (function loop() { 113 | progressTimerId = window.requestAnimationFrame(loop); 114 | topbar.progress( 115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 116 | ); 117 | })(); 118 | } 119 | } 120 | }, 121 | progress: function (to) { 122 | if (typeof to === "undefined") return currentProgress; 123 | if (typeof to === "string") { 124 | to = 125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 126 | ? currentProgress 127 | : 0) + parseFloat(to); 128 | } 129 | currentProgress = to > 1 ? 1 : to; 130 | repaint(); 131 | return currentProgress; 132 | }, 133 | hide: function () { 134 | clearTimeout(delayTimerId); 135 | delayTimerId = null; 136 | if (!showing) return; 137 | showing = false; 138 | if (progressTimerId != null) { 139 | window.cancelAnimationFrame(progressTimerId); 140 | progressTimerId = null; 141 | } 142 | (function loop() { 143 | if (topbar.progress("+.1") >= 1) { 144 | canvas.style.opacity -= 0.05; 145 | if (canvas.style.opacity <= 0.05) { 146 | canvas.style.display = "none"; 147 | fadeTimerId = null; 148 | return; 149 | } 150 | } 151 | fadeTimerId = window.requestAnimationFrame(loop); 152 | })(); 153 | }, 154 | }; 155 | 156 | if (typeof module === "object" && typeof module.exports === "object") { 157 | module.exports = topbar; 158 | } else if (typeof define === "function" && define.amd) { 159 | define(function () { 160 | return topbar; 161 | }); 162 | } else { 163 | this.topbar = topbar; 164 | } 165 | }.call(this, window, document)); 166 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :live_view_native, :platforms, [LiveViewNativeSwiftUi.Platform] 11 | 12 | config :live_view_native, LiveViewNativeSwiftUi.Platform, 13 | app_name: "LVNTutorial", 14 | custom_modifiers: [nav_favorite: LvnTutorialWeb.Modifiers.NavFavorite] 15 | 16 | # Configures the endpoint 17 | config :lvn_tutorial, LvnTutorialWeb.Endpoint, 18 | url: [host: "localhost"], 19 | render_errors: [ 20 | formats: [html: LvnTutorialWeb.ErrorHTML, json: LvnTutorialWeb.ErrorJSON], 21 | layout: false 22 | ], 23 | pubsub_server: LvnTutorial.PubSub, 24 | live_view: [signing_salt: "0p4irQ9b"] 25 | 26 | # Configure esbuild (the version is required) 27 | config :esbuild, 28 | version: "0.17.11", 29 | default: [ 30 | args: 31 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 32 | cd: Path.expand("../assets", __DIR__), 33 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 34 | ] 35 | 36 | # Configure tailwind (the version is required) 37 | config :tailwind, 38 | version: "3.2.7", 39 | default: [ 40 | args: ~w( 41 | --config=tailwind.config.js 42 | --input=css/app.css 43 | --output=../priv/static/assets/app.css 44 | ), 45 | cd: Path.expand("../assets", __DIR__) 46 | ] 47 | 48 | # Configures Elixir's Logger 49 | config :logger, :console, 50 | format: "$time $metadata[$level] $message\n", 51 | metadata: [:request_id] 52 | 53 | # Use Jason for JSON parsing in Phoenix 54 | config :phoenix, :json_library, Jason 55 | 56 | # Import environment specific config. This must remain at the bottom 57 | # of this file so it overrides the configuration defined above. 58 | import_config "#{config_env()}.exs" 59 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with esbuild to bundle .js and .css sources. 9 | config :lvn_tutorial, LvnTutorialWeb.Endpoint, 10 | # Binding to loopback ipv4 address prevents access from other machines. 11 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 12 | http: [ip: {127, 0, 0, 1}, port: 4000], 13 | check_origin: false, 14 | code_reloader: true, 15 | debug_errors: true, 16 | secret_key_base: "cm8PspLDRJB8WPDuePJJPdpYZK7NJP9hOOhM1fNMOL+KCGUXBTgggRElRmIdERQf", 17 | watchers: [ 18 | esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} 19 | ] 20 | 21 | # ## SSL Support 22 | # 23 | # In order to use HTTPS in development, a self-signed 24 | # certificate can be generated by running the following 25 | # Mix task: 26 | # 27 | # mix phx.gen.cert 28 | # 29 | # Run `mix help phx.gen.cert` for more information. 30 | # 31 | # The `http:` config above can be replaced with: 32 | # 33 | # https: [ 34 | # port: 4001, 35 | # cipher_suite: :strong, 36 | # keyfile: "priv/cert/selfsigned_key.pem", 37 | # certfile: "priv/cert/selfsigned.pem" 38 | # ], 39 | # 40 | # If desired, both `http:` and `https:` keys can be 41 | # configured to run both http and https servers on 42 | # different ports. 43 | 44 | # Watch static and templates for browser reloading. 45 | config :lvn_tutorial, LvnTutorialWeb.Endpoint, 46 | live_reload: [ 47 | patterns: [ 48 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 49 | ~r"lib/lvn_tutorial_web/(controllers|live|components)/.*(ex|heex)$" 50 | ] 51 | ] 52 | 53 | # Enable dev routes for dashboard and mailbox 54 | config :lvn_tutorial, dev_routes: true 55 | 56 | # Do not include metadata nor timestamps in development logs 57 | config :logger, :console, format: "[$level] $message\n" 58 | 59 | # Set a higher stacktrace during development. Avoid configuring such 60 | # in production as building large stacktraces may be expensive. 61 | config :phoenix, :stacktrace_depth, 20 62 | 63 | # Initialize plugs at runtime for faster development compilation 64 | config :phoenix, :plug_init_mode, :runtime 65 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix assets.deploy` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :lvn_tutorial, LvnTutorialWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" 13 | 14 | # Do not print debug messages in production 15 | config :logger, level: :info 16 | 17 | # Runtime production configuration, including reading 18 | # of environment variables, is done on config/runtime.exs. 19 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | 10 | # ## Using releases 11 | # 12 | # If you use `mix release`, you need to explicitly enable the server 13 | # by passing the PHX_SERVER=true when you start it: 14 | # 15 | # PHX_SERVER=true bin/lvn_tutorial start 16 | # 17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 | # script that automatically sets the env var above. 19 | if System.get_env("PHX_SERVER") do 20 | config :lvn_tutorial, LvnTutorialWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | # The secret key base is used to sign/encrypt cookies and other secrets. 25 | # A default value is used in config/dev.exs and config/test.exs but you 26 | # want to use a different value for prod and you most likely don't want 27 | # to check this value into version control, so we use an environment 28 | # variable instead. 29 | secret_key_base = 30 | System.get_env("SECRET_KEY_BASE") || 31 | raise """ 32 | environment variable SECRET_KEY_BASE is missing. 33 | You can generate one by calling: mix phx.gen.secret 34 | """ 35 | 36 | host = System.get_env("PHX_HOST") || "example.com" 37 | port = String.to_integer(System.get_env("PORT") || "4000") 38 | 39 | config :lvn_tutorial, LvnTutorialWeb.Endpoint, 40 | url: [host: host, port: 443, scheme: "https"], 41 | http: [ 42 | # Enable IPv6 and bind on all interfaces. 43 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 44 | # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html 45 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 46 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 47 | port: port 48 | ], 49 | secret_key_base: secret_key_base 50 | 51 | # ## SSL Support 52 | # 53 | # To get SSL working, you will need to add the `https` key 54 | # to your endpoint configuration: 55 | # 56 | # config :lvn_tutorial, LvnTutorialWeb.Endpoint, 57 | # https: [ 58 | # ..., 59 | # port: 443, 60 | # cipher_suite: :strong, 61 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 62 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 63 | # ] 64 | # 65 | # The `cipher_suite` is set to `:strong` to support only the 66 | # latest and more secure SSL ciphers. This means old browsers 67 | # and clients may not be supported. You can set it to 68 | # `:compatible` for wider support. 69 | # 70 | # `:keyfile` and `:certfile` expect an absolute path to the key 71 | # and cert in disk or a relative path inside priv, for example 72 | # "priv/ssl/server.key". For all supported SSL configuration 73 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 74 | # 75 | # We also recommend setting `force_ssl` in your endpoint, ensuring 76 | # no data is ever sent via http, always redirecting to https: 77 | # 78 | # config :lvn_tutorial, LvnTutorialWeb.Endpoint, 79 | # force_ssl: [hsts: true] 80 | # 81 | # Check `Plug.SSL` for all available options in `force_ssl`. 82 | end 83 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :lvn_tutorial, LvnTutorialWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "54uBXLgKApT40mU+KRpd/idj79DGR2uuRbfqFHU2haAyXZhOXA109rC6eDDIwwYN", 8 | server: false 9 | 10 | # Print only warnings and errors during test 11 | config :logger, level: :warning 12 | 13 | # Initialize plugs at runtime for faster test compilation 14 | config :phoenix, :plug_init_mode, :runtime 15 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/lib/lvn_tutorial.ex: -------------------------------------------------------------------------------- 1 | defmodule LvnTutorial do 2 | @moduledoc """ 3 | LvnTutorial keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/lib/lvn_tutorial/application.ex: -------------------------------------------------------------------------------- 1 | defmodule LvnTutorial.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | # Start the Telemetry supervisor 12 | LvnTutorialWeb.Telemetry, 13 | # Start the PubSub system 14 | {Phoenix.PubSub, name: LvnTutorial.PubSub}, 15 | # Start the Endpoint (http/https) 16 | LvnTutorialWeb.Endpoint, 17 | # Start a worker by calling: LvnTutorial.Worker.start_link(arg) 18 | # {LvnTutorial.Worker, arg} 19 | LvnTutorial.FavoritesStore 20 | ] 21 | 22 | # See https://hexdocs.pm/elixir/Supervisor.html 23 | # for other strategies and supported options 24 | opts = [strategy: :one_for_one, name: LvnTutorial.Supervisor] 25 | Supervisor.start_link(children, opts) 26 | end 27 | 28 | # Tell Phoenix to update the endpoint configuration 29 | # whenever the application is updated. 30 | @impl true 31 | def config_change(changed, _new, removed) do 32 | LvnTutorialWeb.Endpoint.config_change(changed, removed) 33 | :ok 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/lib/lvn_tutorial/favorites_store.ex: -------------------------------------------------------------------------------- 1 | defmodule LvnTutorial.FavoritesStore do 2 | use GenServer 3 | 4 | # Client 5 | 6 | def start_link(_) do 7 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 8 | end 9 | 10 | def get_favorites() do 11 | GenServer.call(__MODULE__, :get_favorites) 12 | end 13 | 14 | def toggle_favorite(name) do 15 | GenServer.call(__MODULE__, {:toggle_favorite, name}) 16 | end 17 | 18 | def get_score(name) do 19 | GenServer.call(__MODULE__, {:get_score, name}) 20 | end 21 | 22 | def set_score(name, score) do 23 | GenServer.call(__MODULE__, {:set_score, name, score}) 24 | end 25 | 26 | # Server 27 | 28 | @impl true 29 | def init(_) do 30 | {:ok, {[], %{}}} 31 | end 32 | 33 | @impl true 34 | def handle_call(:get_favorites, _from, {favorites, _} = state) do 35 | {:reply, favorites, state} 36 | end 37 | 38 | @impl true 39 | def handle_call({:toggle_favorite, name}, _from, {favorites, scores}) do 40 | new = 41 | if Enum.member?(favorites, name) do 42 | List.delete(favorites, name) 43 | else 44 | [name | favorites] 45 | end 46 | 47 | {:reply, new, {new, scores}} 48 | end 49 | 50 | @impl true 51 | def handle_call({:get_score, name}, _from, {_, scores} = state) do 52 | {:reply, Map.get(scores, name, 0), state} 53 | end 54 | 55 | @impl true 56 | def handle_call({:set_score, name, score}, _from, {favorites, scores}) do 57 | new = Map.put(scores, name, score) 58 | {:reply, new, {favorites, new}} 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/lib/lvn_tutorial_web.ex: -------------------------------------------------------------------------------- 1 | defmodule LvnTutorialWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use LvnTutorialWeb, :controller 9 | use LvnTutorialWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | # Import common connection and controller functions to use in pipelines 27 | import Plug.Conn 28 | import Phoenix.Controller 29 | import Phoenix.LiveView.Router 30 | end 31 | end 32 | 33 | def channel do 34 | quote do 35 | use Phoenix.Channel 36 | end 37 | end 38 | 39 | def controller do 40 | quote do 41 | use Phoenix.Controller, 42 | formats: [:html, :json], 43 | layouts: [html: LvnTutorialWeb.Layouts] 44 | 45 | import Plug.Conn 46 | 47 | unquote(verified_routes()) 48 | end 49 | end 50 | 51 | def live_view do 52 | quote do 53 | use Phoenix.LiveView, 54 | layout: {LvnTutorialWeb.Layouts, :app} 55 | 56 | unquote(html_helpers()) 57 | end 58 | end 59 | 60 | def live_component do 61 | quote do 62 | use Phoenix.LiveComponent 63 | 64 | unquote(html_helpers()) 65 | end 66 | end 67 | 68 | def html do 69 | quote do 70 | use Phoenix.Component 71 | 72 | # Import convenience functions from controllers 73 | import Phoenix.Controller, 74 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 75 | 76 | # Include general helpers for rendering HTML 77 | unquote(html_helpers()) 78 | end 79 | end 80 | 81 | defp html_helpers do 82 | quote do 83 | # HTML escaping functionality 84 | import Phoenix.HTML 85 | # Core UI components and translation 86 | import LvnTutorialWeb.CoreComponents 87 | 88 | # Shortcut for generating JS commands 89 | alias Phoenix.LiveView.JS 90 | 91 | # Routes generation with the ~p sigil 92 | unquote(verified_routes()) 93 | end 94 | end 95 | 96 | def verified_routes do 97 | quote do 98 | use Phoenix.VerifiedRoutes, 99 | endpoint: LvnTutorialWeb.Endpoint, 100 | router: LvnTutorialWeb.Router, 101 | statics: LvnTutorialWeb.static_paths() 102 | end 103 | end 104 | 105 | @doc """ 106 | When used, dispatch to the appropriate controller/view/etc. 107 | """ 108 | defmacro __using__(which) when is_atom(which) do 109 | apply(__MODULE__, which, []) 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lvn_tutorial_backend/lib/lvn_tutorial_web/components/core_components.ex: -------------------------------------------------------------------------------- 1 | defmodule LvnTutorialWeb.CoreComponents do 2 | @moduledoc """ 3 | Provides core UI components. 4 | 5 | At the first glance, this module may seem daunting, but its goal is 6 | to provide some core building blocks in your application, such modals, 7 | tables, and forms. The components are mostly markup and well documented 8 | with doc strings and declarative assigns. You may customize and style 9 | them in any way you want, based on your application growth and needs. 10 | 11 | The default components use Tailwind CSS, a utility-first CSS framework. 12 | See the [Tailwind CSS documentation](https://tailwindcss.com) to learn 13 | how to customize them or feel free to swap in another framework altogether. 14 | 15 | Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. 16 | """ 17 | use Phoenix.Component 18 | 19 | alias Phoenix.LiveView.JS 20 | 21 | @doc """ 22 | Renders a modal. 23 | 24 | ## Examples 25 | 26 | <.modal id="confirm-modal"> 27 | This is a modal. 28 | 29 | 30 | JS commands may be passed to the `:on_cancel` to configure 31 | the closing/cancel event, for example: 32 | 33 | <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> 34 | This is another modal. 35 | 36 | 37 | """ 38 | attr :id, :string, required: true 39 | attr :show, :boolean, default: false 40 | attr :on_cancel, JS, default: %JS{} 41 | slot :inner_block, required: true 42 | 43 | def modal(assigns) do 44 | ~H""" 45 |