diff --git a/.github/workflows/iOS.yml b/.github/workflows/iOS.yml index d0929ad..52d822d 100644 --- a/.github/workflows/iOS.yml +++ b/.github/workflows/iOS.yml @@ -4,12 +4,15 @@ on: [push, pull_request] jobs: build-and-test: - runs-on: macos-latest + runs-on: macos-15 steps: - name: Checkout code uses: actions/checkout@v3 + - name: Select Xcode 26.2 (Swift 6.2 — isolated deinit supported) + run: sudo xcode-select -s /Applications/Xcode_26.2.app + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: @@ -19,17 +22,17 @@ jobs: run: gem install cocoapods - name: Install pod dependencies - working-directory: ./Example + working-directory: ./Examples/Example1 run: pod install || pod install --repo-update - name: Run tests with coverage - working-directory: ./Example + working-directory: ./Examples/Example1 run: | xcodebuild \ -workspace RIBs.xcworkspace \ -scheme RIBs-Example \ -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ + -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation),OS=18.5' \ -enableCodeCoverage YES \ clean test diff --git a/Example/Podfile b/Example/Podfile deleted file mode 100644 index 6c660d9..0000000 --- a/Example/Podfile +++ /dev/null @@ -1,8 +0,0 @@ -use_frameworks! - -platform :ios, '15.0' - -target 'RIBs_Example' do - pod 'RIBs', :path => '../', :testspecs => ['Tests'] - -end diff --git a/Example/.ruby-version b/Examples/Example1/.ruby-version similarity index 100% rename from Example/.ruby-version rename to Examples/Example1/.ruby-version diff --git a/Example/Gemfile b/Examples/Example1/Gemfile similarity index 100% rename from Example/Gemfile rename to Examples/Example1/Gemfile diff --git a/Example/Gemfile.lock b/Examples/Example1/Gemfile.lock similarity index 100% rename from Example/Gemfile.lock rename to Examples/Example1/Gemfile.lock diff --git a/Examples/Example1/Podfile b/Examples/Example1/Podfile new file mode 100644 index 0000000..07090ee --- /dev/null +++ b/Examples/Example1/Podfile @@ -0,0 +1,16 @@ +use_frameworks! + +platform :ios, '15.0' + +target 'RIBs_Example' do + pod 'RIBs', :path => '../../', :testspecs => ['Tests'] + +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' + end + end +end diff --git a/Example/RIBs.xcodeproj/project.pbxproj b/Examples/Example1/RIBs.xcodeproj/project.pbxproj similarity index 100% rename from Example/RIBs.xcodeproj/project.pbxproj rename to Examples/Example1/RIBs.xcodeproj/project.pbxproj diff --git a/Example/RIBs.xcodeproj/xcshareddata/xcschemes/RIBs-Example.xcscheme b/Examples/Example1/RIBs.xcodeproj/xcshareddata/xcschemes/RIBs-Example.xcscheme similarity index 100% rename from Example/RIBs.xcodeproj/xcshareddata/xcschemes/RIBs-Example.xcscheme rename to Examples/Example1/RIBs.xcodeproj/xcshareddata/xcschemes/RIBs-Example.xcscheme diff --git a/Example/RIBs/AppDelegate.swift b/Examples/Example1/RIBs/AppDelegate.swift similarity index 100% rename from Example/RIBs/AppDelegate.swift rename to Examples/Example1/RIBs/AppDelegate.swift diff --git a/Example/RIBs/Base.lproj/LaunchScreen.xib b/Examples/Example1/RIBs/Base.lproj/LaunchScreen.xib similarity index 100% rename from Example/RIBs/Base.lproj/LaunchScreen.xib rename to Examples/Example1/RIBs/Base.lproj/LaunchScreen.xib diff --git a/Example/RIBs/Base.lproj/Main.storyboard b/Examples/Example1/RIBs/Base.lproj/Main.storyboard similarity index 100% rename from Example/RIBs/Base.lproj/Main.storyboard rename to Examples/Example1/RIBs/Base.lproj/Main.storyboard diff --git a/Example/RIBs/Images.xcassets/AppIcon.appiconset/Contents.json b/Examples/Example1/RIBs/Images.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Example/RIBs/Images.xcassets/AppIcon.appiconset/Contents.json rename to Examples/Example1/RIBs/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/Example/RIBs/Info.plist b/Examples/Example1/RIBs/Info.plist similarity index 100% rename from Example/RIBs/Info.plist rename to Examples/Example1/RIBs/Info.plist diff --git a/Example/RIBs/ViewController.swift b/Examples/Example1/RIBs/ViewController.swift similarity index 100% rename from Example/RIBs/ViewController.swift rename to Examples/Example1/RIBs/ViewController.swift diff --git a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d684d1a --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj @@ -0,0 +1,521 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 7E71E73E2F4426FA002FD889 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7E71E73D2F4426FA002FD889 /* RIBs */; }; + 7EA3A6D62F62251D00D01810 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EA3A6D52F62251D00D01810 /* RIBs */; }; + 7EA3A6F42F635A7100D01810 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EA3A6F32F635A7100D01810 /* RIBs */; }; + 7EB78D4F2F12CC0000547345 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EB78D4E2F12CC0000547345 /* RIBs */; }; + 7EFF41982F17D99D00B48704 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EFF41972F17D99D00B48704 /* RIBs */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7EB78D302F12CB3A00547345 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7EB78D112F12CB3800547345 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7EB78D182F12CB3800547345; + remoteInfo = RIBsAppExample2; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 7EB78D192F12CB3800547345 /* RIBsAppExample2.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RIBsAppExample2.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7EB78D2F2F12CB3A00547345 /* RIBsAppExample2Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RIBsAppExample2Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 7EB78D412F12CB3A00547345 /* Exceptions for "RIBsAppExample2" folder in "RIBsAppExample2" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 7EB78D182F12CB3800547345 /* RIBsAppExample2 */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 7EB78D1B2F12CB3800547345 /* RIBsAppExample2 */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 7EB78D412F12CB3A00547345 /* Exceptions for "RIBsAppExample2" folder in "RIBsAppExample2" target */, + ); + path = RIBsAppExample2; + sourceTree = ""; + }; + 7EB78D322F12CB3A00547345 /* RIBsAppExample2Tests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = RIBsAppExample2Tests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7EB78D162F12CB3800547345 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7EA3A6F42F635A7100D01810 /* RIBs in Frameworks */, + 7EA3A6D62F62251D00D01810 /* RIBs in Frameworks */, + 7EFF41982F17D99D00B48704 /* RIBs in Frameworks */, + 7EB78D4F2F12CC0000547345 /* RIBs in Frameworks */, + 7E71E73E2F4426FA002FD889 /* RIBs in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7EB78D2C2F12CB3A00547345 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7EB78D102F12CB3800547345 = { + isa = PBXGroup; + children = ( + 7EB78D1B2F12CB3800547345 /* RIBsAppExample2 */, + 7EB78D322F12CB3A00547345 /* RIBsAppExample2Tests */, + 7EB78D1A2F12CB3800547345 /* Products */, + ); + sourceTree = ""; + }; + 7EB78D1A2F12CB3800547345 /* Products */ = { + isa = PBXGroup; + children = ( + 7EB78D192F12CB3800547345 /* RIBsAppExample2.app */, + 7EB78D2F2F12CB3A00547345 /* RIBsAppExample2Tests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7EB78D182F12CB3800547345 /* RIBsAppExample2 */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7EB78D422F12CB3A00547345 /* Build configuration list for PBXNativeTarget "RIBsAppExample2" */; + buildPhases = ( + 7EB78D152F12CB3800547345 /* Sources */, + 7EB78D162F12CB3800547345 /* Frameworks */, + 7EB78D172F12CB3800547345 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 7EB78D1B2F12CB3800547345 /* RIBsAppExample2 */, + ); + name = RIBsAppExample2; + packageProductDependencies = ( + 7EB78D4E2F12CC0000547345 /* RIBs */, + 7EFF41972F17D99D00B48704 /* RIBs */, + 7E71E73D2F4426FA002FD889 /* RIBs */, + 7EA3A6D52F62251D00D01810 /* RIBs */, + 7EA3A6F32F635A7100D01810 /* RIBs */, + ); + productName = RIBsAppExample2; + productReference = 7EB78D192F12CB3800547345 /* RIBsAppExample2.app */; + productType = "com.apple.product-type.application"; + }; + 7EB78D2E2F12CB3A00547345 /* RIBsAppExample2Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7EB78D472F12CB3A00547345 /* Build configuration list for PBXNativeTarget "RIBsAppExample2Tests" */; + buildPhases = ( + 7EB78D2B2F12CB3A00547345 /* Sources */, + 7EB78D2C2F12CB3A00547345 /* Frameworks */, + 7EB78D2D2F12CB3A00547345 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7EB78D312F12CB3A00547345 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 7EB78D322F12CB3A00547345 /* RIBsAppExample2Tests */, + ); + name = RIBsAppExample2Tests; + packageProductDependencies = ( + ); + productName = RIBsAppExample2Tests; + productReference = 7EB78D2F2F12CB3A00547345 /* RIBsAppExample2Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7EB78D112F12CB3800547345 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + 7EB78D182F12CB3800547345 = { + CreatedOnToolsVersion = 16.4; + }; + 7EB78D2E2F12CB3A00547345 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 7EB78D182F12CB3800547345; + }; + }; + }; + buildConfigurationList = 7EB78D142F12CB3800547345 /* Build configuration list for PBXProject "RIBsAppExample2" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7EB78D102F12CB3800547345; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 7EA3A6F22F635A7100D01810 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 7EB78D1A2F12CB3800547345 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7EB78D182F12CB3800547345 /* RIBsAppExample2 */, + 7EB78D2E2F12CB3A00547345 /* RIBsAppExample2Tests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7EB78D172F12CB3800547345 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7EB78D2D2F12CB3A00547345 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7EB78D152F12CB3800547345 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7EB78D2B2F12CB3A00547345 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7EB78D312F12CB3A00547345 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7EB78D182F12CB3800547345 /* RIBsAppExample2 */; + targetProxy = 7EB78D302F12CB3A00547345 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 7EB78D432F12CB3A00547345 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = A6WM5VZ38B; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = RIBsAppExample2/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7EB78D442F12CB3A00547345 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = A6WM5VZ38B; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = RIBsAppExample2/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7EB78D452F12CB3A00547345 /* 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; + DEVELOPMENT_TEAM = A6WM5VZ38B; + 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 = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7EB78D462F12CB3A00547345 /* 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"; + DEVELOPMENT_TEAM = A6WM5VZ38B; + 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 = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7EB78D482F12CB3A00547345 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = A6WM5VZ38B; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RIBsAppExample2.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/RIBsAppExample2"; + }; + name = Debug; + }; + 7EB78D492F12CB3A00547345 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = A6WM5VZ38B; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RIBsAppExample2.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/RIBsAppExample2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7EB78D142F12CB3800547345 /* Build configuration list for PBXProject "RIBsAppExample2" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7EB78D452F12CB3A00547345 /* Debug */, + 7EB78D462F12CB3A00547345 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7EB78D422F12CB3A00547345 /* Build configuration list for PBXNativeTarget "RIBsAppExample2" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7EB78D432F12CB3A00547345 /* Debug */, + 7EB78D442F12CB3A00547345 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7EB78D472F12CB3A00547345 /* Build configuration list for PBXNativeTarget "RIBsAppExample2Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7EB78D482F12CB3A00547345 /* Debug */, + 7EB78D492F12CB3A00547345 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 7EA3A6F22F635A7100D01810 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../../RIBs-iOS"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 7E71E73D2F4426FA002FD889 /* RIBs */ = { + isa = XCSwiftPackageProductDependency; + productName = RIBs; + }; + 7EA3A6D52F62251D00D01810 /* RIBs */ = { + isa = XCSwiftPackageProductDependency; + productName = RIBs; + }; + 7EA3A6F32F635A7100D01810 /* RIBs */ = { + isa = XCSwiftPackageProductDependency; + productName = RIBs; + }; + 7EB78D4E2F12CC0000547345 /* RIBs */ = { + isa = XCSwiftPackageProductDependency; + productName = RIBs; + }; + 7EFF41972F17D99D00B48704 /* RIBs */ = { + isa = XCSwiftPackageProductDependency; + productName = RIBs; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 7EB78D112F12CB3800547345 /* Project object */; +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/AppComponent.swift b/Examples/RIBsAppExample2/RIBsAppExample2/AppComponent.swift new file mode 100644 index 0000000..71296dc --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/AppComponent.swift @@ -0,0 +1,9 @@ +import RIBs + +class AppComponent: Component, RootDependency { + + init() { + super.init(dependency: EmptyComponent()) + } + +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/AppDelegate.swift b/Examples/RIBsAppExample2/RIBsAppExample2/AppDelegate.swift new file mode 100644 index 0000000..b00f3af --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/Contents.json b/Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Base.lproj/LaunchScreen.storyboard b/Examples/RIBsAppExample2/RIBsAppExample2/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Base.lproj/Main.storyboard b/Examples/RIBsAppExample2/RIBsAppExample2/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/ActorService.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/ActorService.swift new file mode 100644 index 0000000..6bd66cd --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/ActorService.swift @@ -0,0 +1,25 @@ +// +// ActorService.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import Foundation + +protocol ActorServicable: Actor { + func doWork() async +} + +actor ActorService: ActorServicable { + func doWork() async { + printCurrentThread() + try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds + printCurrentThread() + } + + private func printCurrentThread() { + print("Running on: \(Thread.current)") + } +} + diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/AuthService.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/AuthService.swift new file mode 100644 index 0000000..ed253b7 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/AuthService.swift @@ -0,0 +1,18 @@ +// +// AuthService.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +protocol AuthServiceType { + func login() async throws -> UserSession +} + +final class FakeAuthService: AuthServiceType { + + func login() async throws -> UserSession { + try await Task.sleep(nanoseconds: 2_000_000_000) + return UserSession(userId: "u_42", username: "alexvbush", authToken: "tok_abc123") + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift new file mode 100644 index 0000000..8c23065 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift @@ -0,0 +1,64 @@ +// +// FirstViewableRIBBuilder.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs + +protocol FirstViewableRIBDependency: Dependency { + // TODO: Declare the set of dependencies required by this RIB, but cannot be + // created by this RIB. +} + +final class FirstViewableRIBComponent: Component, SecondViewableRIBDependency, FourthViewableRIBDependency, MainRIBDependency { + + var actorService: ActorServicable { + ActorService() + } + + var rxSwiftService: RxSwiftServicable { + RxSwiftService() + } + + var secondViewableRIBBuilder: SecondViewableRIBBuildable { + SecondViewableRIBBuilder(dependency: self) + } + + var fourthViewableRIBBuilder: FourthViewableRIBBuildable { + FourthViewableRIBBuilder(dependency: self) + } + + var authService: AuthServiceType { + shared { FakeAuthService() } + } + + var mainRIBBuilder: MainRIBBuildable { + MainRIBBuilder { (userSession: UserSession) -> MainRIBComponent in + MainRIBComponent(dependency: self, userSession: userSession) + } + } +} + +// MARK: - Builder + +protocol FirstViewableRIBBuildable: Buildable { + func build(withListener listener: FirstViewableRIBListener) -> (routing: FirstViewableRIBRouting, actionableItem: FirstViewableRIBActionableItem) +} + +final class FirstViewableRIBBuilder: Builder, FirstViewableRIBBuildable { + + override init(dependency: FirstViewableRIBDependency) { + super.init(dependency: dependency) + } + + func build(withListener listener: FirstViewableRIBListener) -> (routing: FirstViewableRIBRouting, actionableItem: FirstViewableRIBActionableItem) { + let component = FirstViewableRIBComponent(dependency: dependency) + let viewController = FirstViewableRIBViewController() + let interactor = FirstViewableRIBInteractor(presenter: viewController, actorService: component.actorService, rxSwiftService: component.rxSwiftService, authService: component.authService) + interactor.listener = listener + let router = FirstViewableRIBRouter(interactor: interactor, viewController: viewController, secondViewableRIBBuilder: component.secondViewableRIBBuilder, fourthViewableRIBBuilder: component.fourthViewableRIBBuilder, mainRIBBuilder: component.mainRIBBuilder) + return (routing: router, actionableItem: interactor) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift new file mode 100644 index 0000000..7a2d37b --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift @@ -0,0 +1,140 @@ +// +// FirstViewableRIBInteractor.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift +import Foundation + + +protocol FirstViewableRIBRouting: ViewableRouting { + var firstViewableRIBViewController: FirstViewableRIBViewControllable { get } + func routeToSecondViewableRIB() + func routeAwayFromSecondViewableRIB() + func routeToFourthViewableRIB() -> FourthViewableRIBActionableItem + func routeAwayFromFourthViewableRIB() + func routeToMainRIB(userSession: UserSession) + func routeAwayFromMainRIB() +} + +protocol FirstViewableRIBPresentable: Presentable { + var listener: FirstViewableRIBPresentableListener? { get set } + // TODO: Declare methods the interactor can invoke the presenter to present data. +} + +protocol FirstViewableRIBListener: AnyObject { + // TODO: Declare methods the interactor can invoke to communicate with other RIBs. +} + +final class FirstViewableRIBInteractor: PresentableInteractor, FirstViewableRIBInteractable, FirstViewableRIBPresentableListener { + + weak var router: FirstViewableRIBRouting? + weak var listener: FirstViewableRIBListener? + + private let actorService: ActorServicable + private let rxSwiftService: RxSwiftServicable + private let authService: AuthServiceType + + init(presenter: FirstViewableRIBPresentable, actorService: ActorServicable, rxSwiftService: RxSwiftServicable, authService: AuthServiceType) { + self.actorService = actorService + self.rxSwiftService = rxSwiftService + self.authService = authService + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + + Single + .timer(.seconds(3), scheduler: ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .observe(on: MainScheduler.instance) + .subscribe(onSuccess: { _ in + self.printCurrentThread() + self.router?.routeToSecondViewableRIB() + self.printCurrentThread() + }) + .disposeOnDeactivate(interactor: self) + + rxSwiftService.doWork() + .observe(on: MainScheduler.instance) + .subscribe(onSuccess: { value in + self.printCurrentThread() + print("RxSwiftService.doWork() completed with value: \(value)") + self.printCurrentThread() + }) + .disposeOnDeactivate(interactor: self) + + Task { + await someAsyncWork() + } + + Task { + await someAsyncWork2() + } + } + + private func someAsyncWork() async { + printCurrentThread() + try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds + printCurrentThread() + } + + private func someAsyncWork2() async { + printCurrentThread() + await actorService.doWork() + printCurrentThread() + } + + private func printCurrentThread() { + print("Running on: \(Thread.current)") + } + + func didComplete(_ secondViewableRIB: SecondViewableRIBInteractable) { + router?.routeAwayFromSecondViewableRIB() + } + + // MARK: - FirstViewableRIBPresentableListener + + func login() { + Single.create { [authService] single in + Task { + do { + let session = try await authService.login() + single(.success(session)) + } catch { + single(.failure(error)) + } + } + return Disposables.create() + } + .observe(on: MainScheduler.instance) + .subscribe(onSuccess: { session in + self.router?.routeToMainRIB(userSession: session) + }) + .disposeOnDeactivate(interactor: self) + } + + // MARK: - MainRIBListener + + func didCompleteWithLogout(_ interactor: any MainRIBInteractable) { + router?.routeAwayFromMainRIB() + } + + // MARK: - FirstViewableRIBActionableItem + + func openFourthViewableRIB() -> Observable<(FourthViewableRIBActionableItem, ())> { + guard let fourthItem = router?.routeToFourthViewableRIB() else { + return .empty() + } + return .just((fourthItem, ())) + } + + override func willResignActive() { + super.willResignActive() + // TODO: Pause any business logic. + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift new file mode 100644 index 0000000..cf23295 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift @@ -0,0 +1,95 @@ +// +// FirstViewableRIBRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs + +protocol FirstViewableRIBInteractable: Interactable, FirstViewableRIBActionableItem, SecondViewableRIBListener, FourthViewableRIBListener, MainRIBListener { + var router: FirstViewableRIBRouting? { get set } + var listener: FirstViewableRIBListener? { get set } +} + +protocol FirstViewableRIBViewControllable: ViewControllable { + +} + +final class FirstViewableRIBRouter: ViewableRouter, FirstViewableRIBRouting { + + private let secondViewableRIBBuilder: SecondViewableRIBBuildable + private var secondViewableRIBRouter: SecondViewableRIBRouting? + + private let fourthViewableRIBBuilder: FourthViewableRIBBuildable + private var fourthViewableRIBRouter: FourthViewableRIBRouting? + + private let mainRIBBuilder: MainRIBBuildable + private var mainRIBRouter: MainRIBRouting? + + init(interactor: FirstViewableRIBInteractable, viewController: FirstViewableRIBViewControllable, secondViewableRIBBuilder: SecondViewableRIBBuildable, fourthViewableRIBBuilder: FourthViewableRIBBuildable, mainRIBBuilder: MainRIBBuildable) { + self.secondViewableRIBBuilder = secondViewableRIBBuilder + self.fourthViewableRIBBuilder = fourthViewableRIBBuilder + self.mainRIBBuilder = mainRIBBuilder + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } + + var firstViewableRIBViewController: any FirstViewableRIBViewControllable { + viewController + } + + func routeToSecondViewableRIB() { + let secondViewableRIBRouter = secondViewableRIBBuilder.build(withListener: interactor) + self.secondViewableRIBRouter = secondViewableRIBRouter + let secondViewableRIBViewControllable = secondViewableRIBRouter.secondViewableRIBViewController + attachChild(secondViewableRIBRouter) + viewController.uiviewController.navigationController?.pushViewController(secondViewableRIBViewControllable.uiviewController, animated: true) + } + + func routeAwayFromSecondViewableRIB() { + if let secondViewableRIBRouter = secondViewableRIBRouter { + self.secondViewableRIBRouter = nil + viewController.uiviewController.navigationController?.popToViewController(viewController.uiviewController, animated: true) + detachChild(secondViewableRIBRouter) + } + } + + func routeToFourthViewableRIB() -> FourthViewableRIBActionableItem { + let (fourthViewableRIBRouter, actionableItem) = fourthViewableRIBBuilder.build(withListener: interactor) + self.fourthViewableRIBRouter = fourthViewableRIBRouter + attachChild(fourthViewableRIBRouter) + viewController.uiviewController.navigationController?.pushViewController(fourthViewableRIBRouter.viewControllable.uiviewController, animated: true) + return actionableItem + } + + func routeAwayFromFourthViewableRIB() { + if let fourthViewableRIBRouter = fourthViewableRIBRouter { + self.fourthViewableRIBRouter = nil + viewController.uiviewController.navigationController?.popToViewController(viewController.uiviewController, animated: true) + detachChild(fourthViewableRIBRouter) + } + } + + func routeToMainRIB(userSession: UserSession) { + let mainRIBRouter = mainRIBBuilder.build( + withDynamicBuildDependency: interactor, + dynamicComponentDependency: userSession + ) + self.mainRIBRouter = mainRIBRouter + viewController.uiviewController.navigationController?.pushViewController( + mainRIBRouter.viewControllable.uiviewController, animated: true + ) + attachChild(mainRIBRouter) + } + + func routeAwayFromMainRIB() { + if let mainRIBRouter = mainRIBRouter { + self.mainRIBRouter = nil + viewController.uiviewController.navigationController?.popToViewController( + viewController.uiviewController, animated: true + ) + detachChild(mainRIBRouter) + } + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBViewController.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBViewController.swift new file mode 100644 index 0000000..d559969 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBViewController.swift @@ -0,0 +1,47 @@ +// +// FirstViewableRIBViewController.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift +import UIKit + +protocol FirstViewableRIBPresentableListener: AnyObject { + func login() +} + +final class FirstViewableRIBViewController: UIViewController, FirstViewableRIBPresentable, FirstViewableRIBViewControllable { + + weak var listener: FirstViewableRIBPresentableListener? + + private let loginButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Login", for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemGreen + title = "First" + setupViews() + } + + private func setupViews() { + view.addSubview(loginButton) + NSLayoutConstraint.activate([ + loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + loginButton.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + loginButton.addTarget(self, action: #selector(loginTapped), for: .touchUpInside) + } + + @objc private func loginTapped() { + listener?.login() + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/RxSwiftService.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/RxSwiftService.swift new file mode 100644 index 0000000..a637263 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/RxSwiftService.swift @@ -0,0 +1,36 @@ +// +// RxSwiftService.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import Foundation +import RxSwift + +protocol RxSwiftServicable { + func doWork() -> Single +} + +final class RxSwiftService: RxSwiftServicable { + private let backgroundScheduler = ConcurrentDispatchQueueScheduler(qos: .userInitiated) + + func doWork() -> Single { + return Single + .timer(.seconds(3), scheduler: backgroundScheduler) + .map { @Sendable _ in 42 } + +// return Single.create { @Sendable single in +// // Replace .main with .global() for background execution +// DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2) { +// // This closure now executes on a background thread +// print("Executing on thread: \(Thread.current)") +// single(.success(123)) +// } +// +// return Disposables.create() +// } +// .subscribe(on: backgroundScheduler) + } +} + diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift new file mode 100644 index 0000000..6cfb884 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift @@ -0,0 +1,41 @@ +// +// FourthViewableRIBBuilder.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/10/26. +// + +import RIBs + +protocol FourthViewableRIBDependency: Dependency { + // TODO: Declare the set of dependencies required by this RIB, but cannot be + // created by this RIB. +} + +final class FourthViewableRIBComponent: Component { + + // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. +} + +// MARK: - Builder + +protocol FourthViewableRIBBuildable: Buildable { + func build(withListener listener: FourthViewableRIBListener) -> (routing: FourthViewableRIBRouting, actionableItem: FourthViewableRIBActionableItem) +} + +final class FourthViewableRIBBuilder: Builder, FourthViewableRIBBuildable { + + override init(dependency: FourthViewableRIBDependency) { + super.init(dependency: dependency) + } + + func build(withListener listener: FourthViewableRIBListener) -> (routing: FourthViewableRIBRouting, actionableItem: FourthViewableRIBActionableItem) { + let component = FourthViewableRIBComponent(dependency: dependency) + let viewController = FourthViewableRIBViewController() + let presenter = FourthViewableRIBPresenter(viewController: viewController) + let interactor = FourthViewableRIBInteractor(presenter: presenter) + interactor.listener = listener + let router = FourthViewableRIBRouter(interactor: interactor, viewController: viewController) + return (routing: router, actionableItem: interactor) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBInteractor.swift new file mode 100644 index 0000000..b0150b7 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBInteractor.swift @@ -0,0 +1,55 @@ +// +// FourthViewableRIBInteractor.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/10/26. +// + +import RIBs +import RxSwift + +protocol FourthViewableRIBRouting: ViewableRouting { + // TODO: Declare methods the interactor can invoke to manage sub-tree via the router. +} + +protocol FourthViewableRIBPresentable: Presentable { + var listener: FourthViewableRIBPresentableListener? { get set } + + func presentSomeStuff() +} + +protocol FourthViewableRIBListener: AnyObject { + // TODO: Declare methods the interactor can invoke to communicate with other RIBs. +} + +final class FourthViewableRIBInteractor: PresentableInteractor, FourthViewableRIBInteractable, FourthViewableRIBPresentableListener { + + weak var router: FourthViewableRIBRouting? + weak var listener: FourthViewableRIBListener? + + private let backgroundScheduler = ConcurrentDispatchQueueScheduler(qos: .userInitiated) + + // TODO: Add additional dependencies to constructor. Do not perform any logic + // in constructor. + override init(presenter: FourthViewableRIBPresentable) { + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + + Single + .timer(.seconds(4), scheduler: backgroundScheduler) + .observe(on: MainScheduler.instance) + .subscribe(onSuccess: { _ in + self.presenter.presentSomeStuff() + }) + .disposeOnDeactivate(interactor: self) + } + + override func willResignActive() { + super.willResignActive() + // TODO: Pause any business logic. + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBPresenter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBPresenter.swift new file mode 100644 index 0000000..7c4d99e --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBPresenter.swift @@ -0,0 +1,23 @@ +// +// FourthViewableRIBPresenter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/10/26. +// + +import RIBs + +protocol FourthViewableRIBPresentableListener: AnyObject { + // TODO: Declare properties and methods that the view controller can invoke to perform + // business logic, such as signIn(). This protocol is implemented by the corresponding + // interactor class. +} + +final class FourthViewableRIBPresenter: Presenter, FourthViewableRIBPresentable { + + weak var listener: FourthViewableRIBPresentableListener? + + func presentSomeStuff() { + viewController.renderSomeOtherColor() + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBRouter.swift new file mode 100644 index 0000000..4bb760b --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBRouter.swift @@ -0,0 +1,26 @@ +// +// FourthViewableRIBRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/10/26. +// + +import RIBs + +protocol FourthViewableRIBInteractable: Interactable, FourthViewableRIBActionableItem { + var router: FourthViewableRIBRouting? { get set } + var listener: FourthViewableRIBListener? { get set } +} + +protocol FourthViewableRIBViewControllable: ViewControllable { + func renderSomeOtherColor() +} + +final class FourthViewableRIBRouter: ViewableRouter, FourthViewableRIBRouting { + + // TODO: Constructor inject child builder protocols to allow building children. + override init(interactor: FourthViewableRIBInteractable, viewController: FourthViewableRIBViewControllable) { + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBViewController.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBViewController.swift new file mode 100644 index 0000000..6a47376 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBViewController.swift @@ -0,0 +1,29 @@ +// +// FourthViewableRIBViewController.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/10/26. +// + +import RIBs +import RxSwift +import UIKit + +protocol FourthViewableRIBViewControllableDelegate: AnyObject { + +} + +final class FourthViewableRIBViewController: UIViewController, FourthViewableRIBViewControllable { + + weak var delegate: FourthViewableRIBViewControllableDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemRed + } + + func renderSomeOtherColor() { + view.backgroundColor = .systemPurple + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBBuilder.swift new file mode 100644 index 0000000..dd0b466 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBBuilder.swift @@ -0,0 +1,46 @@ +// +// HomeRIBBuilder.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +import RIBs + +// MARK: - Dependency + +protocol HomeRIBDependency: Dependency { + var currentUserService: CurrentUserServiceType { get } +} + +// MARK: - Component + +final class HomeRIBComponent: Component { + + var currentUserService: CurrentUserServiceType { + dependency.currentUserService + } +} + +// MARK: - Buildable + +protocol HomeRIBBuildable: Buildable { + func build(withListener listener: HomeRIBListener) -> HomeRIBRouting +} + +// MARK: - Builder + +final class HomeRIBBuilder: Builder, HomeRIBBuildable { + + override init(dependency: HomeRIBDependency) { + super.init(dependency: dependency) + } + + func build(withListener listener: HomeRIBListener) -> HomeRIBRouting { + let component = HomeRIBComponent(dependency: dependency) + let viewController = HomeRIBViewController() + let interactor = HomeRIBInteractor(presenter: viewController, currentUserService: component.currentUserService) + interactor.listener = listener + return HomeRIBRouter(interactor: interactor, viewController: viewController) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBInteractor.swift new file mode 100644 index 0000000..1e82074 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBInteractor.swift @@ -0,0 +1,48 @@ +// +// HomeRIBInteractor.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +import RIBs + +protocol HomeRIBRouting: ViewableRouting {} + +protocol HomeRIBPresentable: Presentable { + var listener: HomeRIBPresentableListener? { get set } + func presentUsername(_ username: String) +} + +protocol HomeRIBListener: AnyObject { + func didCompleteHomeByRequestingLogout(_ interactor: HomeRIBInteractable) +} + +final class HomeRIBInteractor: PresentableInteractor, HomeRIBInteractable, HomeRIBPresentableListener { + + weak var router: HomeRIBRouting? + weak var listener: HomeRIBListener? + + private let currentUserService: CurrentUserServiceType + + init(presenter: HomeRIBPresentable, currentUserService: CurrentUserServiceType) { + self.currentUserService = currentUserService + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + presenter.presentUsername(currentUserService.session.username) + } + + override func willResignActive() { + super.willResignActive() + } + + // MARK: - HomeRIBPresentableListener + + func logout() { + listener?.didCompleteHomeByRequestingLogout(self) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBRouter.swift new file mode 100644 index 0000000..6c3b7ec --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBRouter.swift @@ -0,0 +1,23 @@ +// +// HomeRIBRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +import RIBs + +protocol HomeRIBInteractable: Interactable { + var router: HomeRIBRouting? { get set } + var listener: HomeRIBListener? { get set } +} + +protocol HomeRIBViewControllable: ViewControllable {} + +final class HomeRIBRouter: ViewableRouter, HomeRIBRouting { + + override init(interactor: HomeRIBInteractable, viewController: HomeRIBViewControllable) { + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBViewController.swift b/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBViewController.swift new file mode 100644 index 0000000..26e85be --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBViewController.swift @@ -0,0 +1,68 @@ +// +// HomeRIBViewController.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +import RIBs +import UIKit + +protocol HomeRIBPresentableListener: AnyObject { + func logout() +} + +final class HomeRIBViewController: UIViewController, HomeRIBPresentable, HomeRIBViewControllable { + + weak var listener: HomeRIBPresentableListener? + + private let usernameLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 20, weight: .medium) + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let logoutButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Logout", for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemTeal + title = "Home" + setupViews() + } + + // MARK: - HomeRIBPresentable + + func presentUsername(_ username: String) { + usernameLabel.text = "Welcome, \(username)!" + } + + // MARK: - Private + + private func setupViews() { + view.addSubview(usernameLabel) + view.addSubview(logoutButton) + + NSLayoutConstraint.activate([ + usernameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + usernameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -30), + + logoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + logoutButton.topAnchor.constraint(equalTo: usernameLabel.bottomAnchor, constant: 20), + ]) + + logoutButton.addTarget(self, action: #selector(logoutTapped), for: .touchUpInside) + } + + @objc private func logoutTapped() { + listener?.logout() + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Info.plist b/Examples/RIBsAppExample2/RIBsAppExample2/Info.plist new file mode 100644 index 0000000..e0c227b --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleURLTypes + + + CFBundleURLSchemes + + ribsappexample2 + + CFBundleURLName + RIBsAppExample2 Deep Link + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/CurrentUserService.swift b/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/CurrentUserService.swift new file mode 100644 index 0000000..303d65c --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/CurrentUserService.swift @@ -0,0 +1,19 @@ +// +// CurrentUserService.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +protocol CurrentUserServiceType: AnyObject { + var session: UserSession { get } +} + +final class CurrentUserService: CurrentUserServiceType { + + let session: UserSession + + init(session: UserSession) { + self.session = session + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBBuilder.swift new file mode 100644 index 0000000..8ffb157 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBBuilder.swift @@ -0,0 +1,49 @@ +// +// MainRIBBuilder.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +import RIBs + +// MARK: - Dependency + +protocol MainRIBDependency: Dependency { + +} + +// MARK: - Component + +final class MainRIBComponent: Component, HomeRIBDependency { + + let currentUserService: CurrentUserServiceType + + init(dependency: MainRIBDependency, userSession: UserSession) { + self.currentUserService = CurrentUserService(session: userSession) + super.init(dependency: dependency) + } + + fileprivate var homeRIBBuilder: HomeRIBBuildable { + HomeRIBBuilder(dependency: self) + } +} + +// MARK: - Buildable + +protocol MainRIBBuildable: Buildable { + func build(withDynamicBuildDependency listener: MainRIBListener, + dynamicComponentDependency userSession: UserSession) -> MainRIBRouting +} + +// MARK: - Builder + +final class MainRIBBuilder: ComponentizedBuilder, MainRIBBuildable { + + override func build(with component: MainRIBComponent, _ listener: MainRIBListener) -> MainRIBRouting { + let viewController = MainRIBViewController() + let interactor = MainRIBInteractor(presenter: viewController, currentUserService: component.currentUserService) + interactor.listener = listener + return MainRIBRouter(interactor: interactor, viewController: viewController, homeRIBBuilder: component.homeRIBBuilder) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBInteractor.swift new file mode 100644 index 0000000..40f5d4d --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBInteractor.swift @@ -0,0 +1,53 @@ +// +// MainRIBInteractor.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +import RIBs + +protocol MainRIBRouting: ViewableRouting { + func routeToHomeRIB() + func routeAwayFromHomeRIB() +} + +protocol MainRIBPresentable: Presentable { + var listener: MainRIBPresentableListener? { get set } +} + +protocol MainRIBListener: AnyObject { + func didCompleteWithLogout(_ interactor: MainRIBInteractable) +} + +final class MainRIBInteractor: PresentableInteractor, MainRIBInteractable, MainRIBPresentableListener { + + weak var router: MainRIBRouting? + weak var listener: MainRIBListener? + + // MainRIBInteractor receives currentUserService directly, but it also lives + // in the component so HomeRIB can access it without going through Main. + private let currentUserService: CurrentUserServiceType + + init(presenter: MainRIBPresentable, currentUserService: CurrentUserServiceType) { + self.currentUserService = currentUserService + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + + self.router?.routeToHomeRIB() + } + + override func willResignActive() { + super.willResignActive() + } + + // MARK: - HomeRIBListener + + func didCompleteHomeByRequestingLogout(_ interactor: any HomeRIBInteractable) { + listener?.didCompleteWithLogout(self) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBRouter.swift new file mode 100644 index 0000000..ddaf041 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBRouter.swift @@ -0,0 +1,44 @@ +// +// MainRIBRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +import RIBs + +protocol MainRIBInteractable: Interactable, HomeRIBListener { + var router: MainRIBRouting? { get set } + var listener: MainRIBListener? { get set } +} + +protocol MainRIBViewControllable: ViewControllable {} + +final class MainRIBRouter: ViewableRouter, MainRIBRouting { + + private let homeRIBBuilder: HomeRIBBuildable + private var homeRIBRouter: HomeRIBRouting? + + init(interactor: MainRIBInteractable, viewController: MainRIBViewControllable, homeRIBBuilder: HomeRIBBuildable) { + self.homeRIBBuilder = homeRIBBuilder + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } + + func routeToHomeRIB() { + let homeRIBRouter = homeRIBBuilder.build(withListener: interactor) + self.homeRIBRouter = homeRIBRouter + viewController.uiviewController.navigationController?.pushViewController( + homeRIBRouter.viewControllable.uiviewController, animated: false + ) + attachChild(homeRIBRouter) + } + + func routeAwayFromHomeRIB() { + if let homeRIBRouter = homeRIBRouter { + self.homeRIBRouter = nil + viewController.uiviewController.navigationController?.popToViewController(viewController.uiviewController, animated: true) + detachChild(homeRIBRouter) + } + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBViewController.swift b/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBViewController.swift new file mode 100644 index 0000000..d97e879 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBViewController.swift @@ -0,0 +1,22 @@ +// +// MainRIBViewController.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +import RIBs +import UIKit + +protocol MainRIBPresentableListener: AnyObject {} + +final class MainRIBViewController: UIViewController, MainRIBPresentable, MainRIBViewControllable { + + weak var listener: MainRIBPresentableListener? + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemIndigo + title = "Main" + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift new file mode 100644 index 0000000..fb1b7ed --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift @@ -0,0 +1,46 @@ +// +// RootBuilder.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs + +protocol RootDependency: Dependency { + // TODO: Declare the set of dependencies required by this RIB, but cannot be + // created by this RIB. +} + +final class RootComponent: Component, FirstViewableRIBDependency { + + var firstViewableRIBBuilder: FirstViewableRIBBuildable { + FirstViewableRIBBuilder(dependency: self) + } +} + +// MARK: - Builder + +struct RootBuildResult { + let launchRouter: LaunchRouting + let urlHandler: UrlHandler +} + +protocol RootBuildable: Buildable { + func build() -> RootBuildResult +} + +final class RootBuilder: Builder, RootBuildable { + + override init(dependency: RootDependency) { + super.init(dependency: dependency) + } + + func build() -> RootBuildResult { + let component = RootComponent(dependency: dependency) + let viewController = RootViewController() + let interactor = RootInteractor(presenter: viewController) + let launchRouter = RootRouter(interactor: interactor, viewController: viewController, firstViewableRIBBuilder: component.firstViewableRIBBuilder) + return RootBuildResult(launchRouter: launchRouter, urlHandler: interactor) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootInteractor.swift new file mode 100644 index 0000000..609e579 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootInteractor.swift @@ -0,0 +1,70 @@ +// +// RootInteractor.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift +import Foundation + +protocol RootRouting: ViewableRouting { + func routeToFirstViewableRIB() -> FirstViewableRIBActionableItem + func routeAwayFromFirstViewableRIB() +} + +protocol RootPresentable: Presentable { + var listener: RootPresentableListener? { get set } + // TODO: Declare methods the interactor can invoke the presenter to present data. +} + +protocol RootListener: AnyObject { + // TODO: Declare methods the interactor can invoke to communicate with other RIBs. +} + +final class RootInteractor: PresentableInteractor, RootInteractable, RootPresentableListener, UrlHandler { + + weak var router: RootRouting? + weak var listener: RootListener? + + private let firstViewableRIBActionableItemSubject = ReplaySubject.create(bufferSize: 1) + + // TODO: Add additional dependencies to constructor. Do not perform any logic + // in constructor. + override init(presenter: RootPresentable) { + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + + if let firstItem = router?.routeToFirstViewableRIB() { + firstViewableRIBActionableItemSubject.onNext(firstItem) + } + } + + override func willResignActive() { + super.willResignActive() + // TODO: Pause any business logic. + } + + // MARK: - RootActionableItem + + func waitForFirstViewableRIB() -> Observable<(FirstViewableRIBActionableItem, ())> { + return firstViewableRIBActionableItemSubject.map { ($0, ()) } + } + + // MARK: - UrlHandler + + func handle(_ url: URL) { + switch url.path { + case "/example-deeplink": + let workflow = OpenFourthViewableRIBWorkflow() + workflow.subscribe(self).disposeOnDeactivate(interactor: self) + default: + break + } + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootRouter.swift new file mode 100644 index 0000000..b581397 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootRouter.swift @@ -0,0 +1,48 @@ +// +// RootRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs + +protocol RootInteractable: Interactable, FirstViewableRIBListener, RootActionableItem { + var router: RootRouting? { get set } + var listener: RootListener? { get set } +} + +protocol RootViewControllable: ViewControllable { + func embedMainView(_ viewControllable: ViewControllable) + func removeMainView(_ viewControllable: ViewControllable) +} + +final class RootRouter: LaunchRouter, RootRouting { + + private let firstViewableRIBBuilder: FirstViewableRIBBuildable + private var firstViewableRIBRouter: FirstViewableRIBRouting? + + init(interactor: RootInteractable, viewController: RootViewControllable, firstViewableRIBBuilder: FirstViewableRIBBuildable) { + self.firstViewableRIBBuilder = firstViewableRIBBuilder + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } + + func routeToFirstViewableRIB() -> FirstViewableRIBActionableItem { + let (firstViewableRIBRouter, actionableItem) = firstViewableRIBBuilder.build(withListener: interactor) + self.firstViewableRIBRouter = firstViewableRIBRouter + let firstViewableRIBViewController = firstViewableRIBRouter.firstViewableRIBViewController + viewController.embedMainView(firstViewableRIBViewController) + attachChild(firstViewableRIBRouter) + return actionableItem + } + + func routeAwayFromFirstViewableRIB() { + if let firstViewableRIBRouter = firstViewableRIBRouter { + self.firstViewableRIBRouter = nil + let firstViewableRIBViewController = firstViewableRIBRouter.firstViewableRIBViewController + viewController.removeMainView(firstViewableRIBViewController) + detachChild(firstViewableRIBRouter) + } + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootViewController.swift b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootViewController.swift new file mode 100644 index 0000000..77efd27 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootViewController.swift @@ -0,0 +1,44 @@ +// +// RootViewController.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift +import UIKit + +protocol RootPresentableListener: AnyObject { + // TODO: Declare properties and methods that the view controller can invoke to perform + // business logic, such as signIn(). This protocol is implemented by the corresponding + // interactor class. +} + +final class RootViewController: UIViewController, RootPresentable, RootViewControllable { + + weak var listener: RootPresentableListener? + + func embedMainView(_ viewControllable: ViewControllable) { + let navController = UINavigationController(rootViewController: viewControllable.uiviewController) + + addChild(navController) + view.addSubview(navController.view) + navController.didMove(toParent: self) + let childView = navController.view! + childView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + childView.topAnchor.constraint(equalTo: self.view.topAnchor), + childView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), + childView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), + childView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) + ]) + } + + func removeMainView(_ viewControllable: ViewControllable) { + guard let navController = viewControllable.uiviewController.navigationController else { return } + navController.willMove(toParent: nil) + navController.view.removeFromSuperview() + navController.removeFromParent() + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SceneDelegate.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SceneDelegate.swift new file mode 100644 index 0000000..75d5620 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SceneDelegate.swift @@ -0,0 +1,37 @@ +// +// SceneDelegate.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import UIKit +import RIBs + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + private var launchRouter: LaunchRouting? + private var urlHandler: UrlHandler? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + let appComponent = AppComponent() + + let window = UIWindow(windowScene: windowScene) + self.window = window + + let result = RootBuilder(dependency: appComponent).build() + launchRouter = result.launchRouter + urlHandler = result.urlHandler + result.launchRouter.launch(from: window) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + if let url = URLContexts.first?.url { + urlHandler?.handle(url) + } + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/ExampleWorker.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/ExampleWorker.swift new file mode 100644 index 0000000..7355bc4 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/ExampleWorker.swift @@ -0,0 +1,28 @@ +// +// ExampleWorker.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift +import Foundation + +protocol ExampleWorker: Working {} + +final class ExampleWorkerImp: Worker, ExampleWorker { + + private let backgroundScheduler = ConcurrentDispatchQueueScheduler(qos: .userInitiated) + + override func didStart(_ interactorScope: InteractorScope) { + Single + .timer(.seconds(3), scheduler: backgroundScheduler) + .map { @Sendable _ in "mock response" } + .observe(on: MainScheduler.instance) + .subscribe(onSuccess: { value in + print("ExampleWorker: Single fired with value: \(value)") + }) + .disposeOnStop(self) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift new file mode 100644 index 0000000..c2eea8e --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift @@ -0,0 +1,50 @@ +// +// SecondViewableRIBBuilder.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs + +protocol SecondViewableRIBDependency: Dependency { + // TODO: Declare the set of dependencies required by this RIB, but cannot be + // created by this RIB. +} + +final class SecondViewableRIBComponent: Component, ThirdHeadlessRIBDependency { + + var thirdHeadlessRIBBuilder: ThirdHeadlessRIBBuildable { + ThirdHeadlessRIBBuilder(dependency: self) + } + + var viewController: SecondViewableRIBPresentable & SecondViewableRIBViewControllable { + shared { SecondViewableRIBViewController() } + } + + var thirdHeadlessRIBViewController: any ThirdHeadlessRIBViewControllable { + viewController + } +} + +// MARK: - Builder + +protocol SecondViewableRIBBuildable: Buildable { + func build(withListener listener: SecondViewableRIBListener) -> SecondViewableRIBRouting +} + +final class SecondViewableRIBBuilder: Builder, SecondViewableRIBBuildable { + + override init(dependency: SecondViewableRIBDependency) { + super.init(dependency: dependency) + } + + func build(withListener listener: SecondViewableRIBListener) -> SecondViewableRIBRouting { + let component = SecondViewableRIBComponent(dependency: dependency) + let viewController = component.viewController + let exampleWorker = ExampleWorkerImp() + let interactor = SecondViewableRIBInteractor(presenter: viewController, exampleWorker: exampleWorker) + interactor.listener = listener + return SecondViewableRIBRouter(interactor: interactor, viewController: viewController, thirdHeadlessRIBBuilder: component.thirdHeadlessRIBBuilder) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift new file mode 100644 index 0000000..eb00755 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift @@ -0,0 +1,55 @@ +// +// SecondViewableRIBInteractor.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift + +protocol SecondViewableRIBRouting: ViewableRouting { + var secondViewableRIBViewController: SecondViewableRIBViewControllable { get } +} + +protocol SecondViewableRIBPresentable: Presentable { + var listener: SecondViewableRIBPresentableListener? { get set } + // TODO: Declare methods the interactor can invoke the presenter to present data. +} + +protocol SecondViewableRIBListener: AnyObject { + func didComplete(_ secondViewableRIB: SecondViewableRIBInteractable) +} + +final class SecondViewableRIBInteractor: PresentableInteractor, SecondViewableRIBInteractable, SecondViewableRIBPresentableListener { + + weak var router: SecondViewableRIBRouting? + weak var listener: SecondViewableRIBListener? + + private let exampleWorker: ExampleWorker + + init(presenter: SecondViewableRIBPresentable, exampleWorker: ExampleWorker) { + self.exampleWorker = exampleWorker + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + exampleWorker.start(self) + } + + func close() { + listener?.didComplete(self) + } + + override func willResignActive() { + super.willResignActive() + + print("SecondViewableRIBInteractor willResignActive") + } + + func sendData(_ interactor: any ThirdHeadlessRIBInteractable) { + print("data received in the SecondViewableRIBInteractor from ThirdHeadlessRIBInteractable") + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBRouter.swift new file mode 100644 index 0000000..df777d5 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBRouter.swift @@ -0,0 +1,45 @@ +// +// SecondViewableRIBRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs + +protocol SecondViewableRIBInteractable: Interactable, ThirdHeadlessRIBListener { + var router: SecondViewableRIBRouting? { get set } + var listener: SecondViewableRIBListener? { get set } +} + +protocol SecondViewableRIBViewControllable: ViewControllable, ThirdHeadlessRIBViewControllable { + +} + +final class SecondViewableRIBRouter: ViewableRouter, SecondViewableRIBRouting { + + private let thirdHeadlessRIBBuilder: ThirdHeadlessRIBBuildable + private var thirdHeadlessRIBRouter: ThirdHeadlessRIBRouting? + + init(interactor: SecondViewableRIBInteractable, viewController: SecondViewableRIBViewControllable, thirdHeadlessRIBBuilder: ThirdHeadlessRIBBuildable) { + self.thirdHeadlessRIBBuilder = thirdHeadlessRIBBuilder + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } + + var secondViewableRIBViewController: any SecondViewableRIBViewControllable { + viewController + } + + override func didLoad() { + super.didLoad() + + routeToThirdHeadlessRIB() + } + + private func routeToThirdHeadlessRIB() { + let thirdHeadlessRIBRouter = thirdHeadlessRIBBuilder.build(withListener: interactor) + self.thirdHeadlessRIBRouter = thirdHeadlessRIBRouter + attachChild(thirdHeadlessRIBRouter) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBViewController.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBViewController.swift new file mode 100644 index 0000000..8f88315 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBViewController.swift @@ -0,0 +1,40 @@ +// +// SecondViewableRIBViewController.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift +import UIKit + +protocol SecondViewableRIBPresentableListener: AnyObject { + func close() +} + +final class SecondViewableRIBViewController: UIViewController, SecondViewableRIBPresentable, SecondViewableRIBViewControllable { + + weak var listener: SecondViewableRIBPresentableListener? + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .blue + setupBackButton() + } + + private func setupBackButton() { + let backButton = UIBarButtonItem( + image: UIImage(systemName: "chevron.left"), + style: .plain, + target: self, + action: #selector(didTapBack) + ) + navigationItem.leftBarButtonItem = backButton + } + + @objc private func didTapBack() { + listener?.close() + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBBuilder.swift new file mode 100644 index 0000000..02049f9 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBBuilder.swift @@ -0,0 +1,46 @@ +// +// ThirdHeadlessRIBBuilder.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/10/26. +// + +import RIBs + +protocol ThirdHeadlessRIBDependency: Dependency { + + var thirdHeadlessRIBViewController: ThirdHeadlessRIBViewControllable { get } + // TODO: Declare the set of dependencies required by this RIB, but won't be + // created by this RIB. +} + +final class ThirdHeadlessRIBComponent: Component, FourthViewableRIBDependency { + + fileprivate var thirdHeadlessRIBViewController: ThirdHeadlessRIBViewControllable { + return dependency.thirdHeadlessRIBViewController + } + + fileprivate var fourthViewableRIBBuilder: FourthViewableRIBBuildable { + FourthViewableRIBBuilder(dependency: self) + } +} + +// MARK: - Builder + +protocol ThirdHeadlessRIBBuildable: Buildable { + func build(withListener listener: ThirdHeadlessRIBListener) -> ThirdHeadlessRIBRouting +} + +final class ThirdHeadlessRIBBuilder: Builder, ThirdHeadlessRIBBuildable { + + override init(dependency: ThirdHeadlessRIBDependency) { + super.init(dependency: dependency) + } + + func build(withListener listener: ThirdHeadlessRIBListener) -> ThirdHeadlessRIBRouting { + let component = ThirdHeadlessRIBComponent(dependency: dependency) + let interactor = ThirdHeadlessRIBInteractor() + interactor.listener = listener + return ThirdHeadlessRIBRouter(interactor: interactor, viewController: component.thirdHeadlessRIBViewController, fourthViewableRIBBuilder: component.fourthViewableRIBBuilder) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBInteractor.swift new file mode 100644 index 0000000..4c00751 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBInteractor.swift @@ -0,0 +1,54 @@ +// +// ThirdHeadlessRIBInteractor.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/10/26. +// + +import RIBs +import RxSwift + +protocol ThirdHeadlessRIBRouting: Routing { + func cleanupViews() + // TODO: Declare methods the interactor can invoke to manage sub-tree via the router. + func routeToFourthRIB() + func routeAwayFromFourthRIB() +} + +protocol ThirdHeadlessRIBListener: AnyObject { + func sendData(_ interactor: ThirdHeadlessRIBInteractable) +} + +final class ThirdHeadlessRIBInteractor: Interactor, ThirdHeadlessRIBInteractable { + + private let backgroundScheduler = ConcurrentDispatchQueueScheduler(qos: .userInitiated) + + weak var router: ThirdHeadlessRIBRouting? + weak var listener: ThirdHeadlessRIBListener? + + // TODO: Add additional dependencies to constructor. Do not perform any logic + // in constructor. + override init() {} + + override func didBecomeActive() { + super.didBecomeActive() + + print("do some work in the ThirdHeadlessRIBInteractor") + + Single + .timer(.seconds(3), scheduler: backgroundScheduler) + .observe(on: MainScheduler.instance) + .subscribe(onSuccess: { _ in + self.listener?.sendData(self) + self.router?.routeToFourthRIB() + }) + .disposeOnDeactivate(interactor: self) + } + + override func willResignActive() { + super.willResignActive() + + router?.cleanupViews() + // TODO: Pause any business logic. + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift new file mode 100644 index 0000000..6ca3acb --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift @@ -0,0 +1,58 @@ +// +// ThirdHeadlessRIBRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/10/26. +// + +import RIBs + +protocol ThirdHeadlessRIBInteractable: Interactable, FourthViewableRIBListener { + var router: ThirdHeadlessRIBRouting? { get set } + var listener: ThirdHeadlessRIBListener? { get set } +} + +protocol ThirdHeadlessRIBViewControllable: ViewControllable { + // TODO: Declare methods the router invokes to manipulate the view hierarchy. Since + // this RIB does not own its own view, this protocol is conformed to by one of this + // RIB's ancestor RIBs' view. +} + +final class ThirdHeadlessRIBRouter: Router, ThirdHeadlessRIBRouting { + + private let viewController: ThirdHeadlessRIBViewControllable + + private let fourthViewableRIBBuilder: FourthViewableRIBBuildable + private var fourthViewableRIBRouter: FourthViewableRIBRouting? + + // TODO: Constructor inject child builder protocols to allow building children. + init(interactor: ThirdHeadlessRIBInteractable, viewController: ThirdHeadlessRIBViewControllable, fourthViewableRIBBuilder: FourthViewableRIBBuildable) { + self.viewController = viewController + self.fourthViewableRIBBuilder = fourthViewableRIBBuilder + super.init(interactor: interactor) + interactor.router = self + } + + func cleanupViews() { + // TODO: Since this router does not own its view, it needs to cleanup the views + // it may have added to the view hierarchy, when its interactor is deactivated. + routeAwayFromFourthRIB() + } + + + func routeToFourthRIB() { + let (fourthViewableRIBRouter, fourthViewableRIBInteractor) = fourthViewableRIBBuilder.build(withListener: interactor) + self.fourthViewableRIBRouter = fourthViewableRIBRouter + let fourthViewableRIBViewControllable = fourthViewableRIBRouter.viewControllable + attachChild(fourthViewableRIBRouter) + viewController.uiviewController.navigationController?.pushViewController(fourthViewableRIBViewControllable.uiviewController, animated: true) + } + + func routeAwayFromFourthRIB() { + if let fourthViewableRIBRouter = fourthViewableRIBRouter { + self.fourthViewableRIBRouter = nil + viewController.uiviewController.navigationController?.popViewController(animated: true) + detachChild(fourthViewableRIBRouter) + } + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/UserSession.swift b/Examples/RIBsAppExample2/RIBsAppExample2/UserSession.swift new file mode 100644 index 0000000..56c8932 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/UserSession.swift @@ -0,0 +1,12 @@ +// +// UserSession.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/12/26. +// + +struct UserSession { + let userId: String + let username: String + let authToken: String +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/ViewController.swift b/Examples/RIBsAppExample2/RIBsAppExample2/ViewController.swift new file mode 100644 index 0000000..371fe7d --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/ViewController.swift @@ -0,0 +1,19 @@ +// +// ViewController.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + +} + diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Workflows/OpenFourthViewableRIBWorkflow.swift b/Examples/RIBsAppExample2/RIBsAppExample2/Workflows/OpenFourthViewableRIBWorkflow.swift new file mode 100644 index 0000000..4cf2096 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Workflows/OpenFourthViewableRIBWorkflow.swift @@ -0,0 +1,47 @@ +// +// OpenFourthViewableRIBWorkflow.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/11/26. +// + +import RIBs +import RxSwift + +// MARK: - ActionableItem Protocols + +/// The root interactor's actionable interface used by workflows. +protocol RootActionableItem: AnyObject { + /// Emits when the First RIB is active and ready to accept workflow steps. + func waitForFirstViewableRIB() -> Observable<(FirstViewableRIBActionableItem, ())> +} + +/// The First RIB interactor's actionable interface used by workflows. +protocol FirstViewableRIBActionableItem: AnyObject { + /// Routes directly to the Fourth RIB, bypassing the Second/Third path. + func openFourthViewableRIB() -> Observable<(FourthViewableRIBActionableItem, ())> +} + +/// The Fourth RIB interactor's actionable interface used by workflows. +protocol FourthViewableRIBActionableItem: AnyObject {} + +// MARK: - Workflow + +/// A workflow triggered by the `ribsappexample2:///example-deeplink` deep link. +/// +/// Steps: +/// 1. Wait for the First RIB to become active (it starts automatically at launch). +/// 2. Route directly from First to Fourth, demonstrating cross-path navigation. +final class OpenFourthViewableRIBWorkflow: Workflow { + override init() { + super.init() + self + .onStep { (rootItem: RootActionableItem) -> Observable<(FirstViewableRIBActionableItem, ())> in + rootItem.waitForFirstViewableRIB() + } + .onStep { (firstItem: FirstViewableRIBActionableItem, _) -> Observable<(FourthViewableRIBActionableItem, ())> in + firstItem.openFourthViewableRIB() + } + .commit() + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Workflows/UrlHandler.swift b/Examples/RIBsAppExample2/RIBsAppExample2/Workflows/UrlHandler.swift new file mode 100644 index 0000000..6395207 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Workflows/UrlHandler.swift @@ -0,0 +1,12 @@ +// +// UrlHandler.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/11/26. +// + +import Foundation + +protocol UrlHandler: AnyObject { + func handle(_ url: URL) +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2Tests/RIBsAppExample2Tests.swift b/Examples/RIBsAppExample2/RIBsAppExample2Tests/RIBsAppExample2Tests.swift new file mode 100644 index 0000000..05a61d7 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2Tests/RIBsAppExample2Tests.swift @@ -0,0 +1,36 @@ +// +// RIBsAppExample2Tests.swift +// RIBsAppExample2Tests +// +// Created by Alex Bush on 1/10/26. +// + +import XCTest +@testable import RIBsAppExample2 + +final class RIBsAppExample2Tests: 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/README.md b/README.md index 2372f11..bf600d2 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ To get more hands on with RIBs, we have written a [series of tutorials](https:// To read about the backstory on why we created RIBs, see [this blog post](https://www.uber.com/blog/new-rider-app-architecture/) we wrote when releasing RIBs in production the first time and see [this short video](https://www.youtube.com/watch?v=Q5cTT0M0YXg) where we discussed how the RIBs architecture works. +If you are adopting Swift 6 strict concurrency in a project that uses RIBs, see the [Swift 6 Strict Concurrency Migration Guide](SWIFT6_STRICT_CONCURRENCY_MIGRATION.md). + #### What is the difference between RIBs and MV*/VIPER? MVC, MVP, MVI, MVVM and VIPER are architecture patterns. RIBs is a framework. What differentiates RIBs from frameworks based on MV*/VIPER is: diff --git a/RIBs.podspec b/RIBs.podspec index 38f8ecd..3f41d28 100644 --- a/RIBs.podspec +++ b/RIBs.podspec @@ -12,8 +12,8 @@ RIBs is the cross-platform architecture behind many mobile apps at Uber. This ar s.ios.deployment_target = '15.0' s.swift_version = '5.0' s.source_files = 'RIBs/Classes/**/*' - s.dependency 'RxSwift', '~> 6.0' - s.dependency 'RxRelay', '~> 6.0' + s.dependency 'RxSwift', '~> 6.9.0' + s.dependency 'RxRelay', '~> 6.9.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'RIBsTests/**/*.swift' diff --git a/RIBs/Classes/Builder.swift b/RIBs/Classes/Builder.swift index 9f16e9d..b04f8b2 100644 --- a/RIBs/Classes/Builder.swift +++ b/RIBs/Classes/Builder.swift @@ -17,9 +17,11 @@ import Foundation /// The base builder protocol that all builders should conform to. +@MainActor public protocol Buildable: AnyObject {} /// Utility that instantiates a RIB and sets up its internal wirings. +@MainActor open class Builder: Buildable { /// The dependency used for this builder to build the RIB. diff --git a/RIBs/Classes/DI/Component.swift b/RIBs/Classes/DI/Component.swift index 4a178ff..aa5995f 100644 --- a/RIBs/Classes/DI/Component.swift +++ b/RIBs/Classes/DI/Component.swift @@ -23,6 +23,7 @@ import Foundation /// /// A component subclass implementation should conform to child 'Dependency' protocols, defined by all of its immediate /// children. +@MainActor open class Component: Dependency { /// The dependency of this `Component`. @@ -70,6 +71,7 @@ open class Component: Dependency { } /// The special empty component. +@MainActor open class EmptyComponent: EmptyDependency { /// Initializer. diff --git a/RIBs/Classes/DI/Dependency.swift b/RIBs/Classes/DI/Dependency.swift index a45cc17..36fff40 100644 --- a/RIBs/Classes/DI/Dependency.swift +++ b/RIBs/Classes/DI/Dependency.swift @@ -20,7 +20,9 @@ import Foundation /// /// Subclasses should define a set of properties that are required by the module from the DI graph. A dependency is /// typically provided and satisfied by its immediate parent module. +@MainActor public protocol Dependency: AnyObject {} /// The special empty dependency. +@MainActor public protocol EmptyDependency: Dependency {} diff --git a/RIBs/Classes/Interactor.swift b/RIBs/Classes/Interactor.swift index d80ded2..d123c79 100644 --- a/RIBs/Classes/Interactor.swift +++ b/RIBs/Classes/Interactor.swift @@ -19,6 +19,7 @@ import RxSwift import UIKit /// Protocol defining the activeness of an interactor's scope. +@MainActor public protocol InteractorScope: AnyObject { // The following properties must be declared in the base protocol, since `Router` internally invokes these methods. @@ -37,6 +38,7 @@ public protocol InteractorScope: AnyObject { } /// The base protocol for all interactors. +@MainActor public protocol Interactable: InteractorScope { // The following methods must be declared in the base protocol, since `Router` internally invokes these methods. @@ -64,6 +66,7 @@ public protocol Interactable: InteractorScope { /// active. /// /// An `Interactor` should only perform its business logic when it's currently active. +@MainActor open class Interactor: Interactable { /// Indicates if the interactor is active. @@ -139,7 +142,7 @@ open class Interactor: Interactable { private let isActiveSubject = BehaviorSubject(value: false) fileprivate var activenessDisposable: CompositeDisposable? - deinit { + isolated deinit { if isActive { deactivate() } @@ -162,7 +165,7 @@ public extension ObservableType { /// /// - parameter interactorScope: The interactor scope whose activeness this observable is confined to. /// - returns: The `Observable` confined to this interactor's activeness lifecycle. - + @MainActor func confineTo(_ interactorScope: InteractorScope) -> Observable { return Observable .combineLatest(interactorScope.isActiveStream, self) { isActive, value in @@ -194,7 +197,7 @@ public extension Disposable { /// terminated. /// /// - parameter interactor: The interactor to dispose the subscription based on. - @discardableResult + @discardableResult @MainActor func disposeOnDeactivate(interactor: Interactor) -> Disposable { if let activenessDisposable = interactor.activenessDisposable { _ = activenessDisposable.insert(self) diff --git a/RIBs/Classes/LaunchRouter.swift b/RIBs/Classes/LaunchRouter.swift index d50dc20..96e58e9 100644 --- a/RIBs/Classes/LaunchRouter.swift +++ b/RIBs/Classes/LaunchRouter.swift @@ -17,6 +17,7 @@ import UIKit /// The root `Router` of an application. +@MainActor public protocol LaunchRouting: ViewableRouting { /// Launches the router tree. @@ -26,6 +27,7 @@ public protocol LaunchRouting: ViewableRouting { } /// The application root router base class, that acts as the root of the router tree. +@MainActor open class LaunchRouter: ViewableRouter, LaunchRouting { /// Initializer. diff --git a/RIBs/Classes/LeakDetector/LeakDetector.swift b/RIBs/Classes/LeakDetector/LeakDetector.swift index 0f797aa..9cf3882 100644 --- a/RIBs/Classes/LeakDetector/LeakDetector.swift +++ b/RIBs/Classes/LeakDetector/LeakDetector.swift @@ -51,6 +51,7 @@ public protocol LeakDetectionHandle { /// A `Router` that owns an `Interactor` might for example expect its `Interactor` be deallocated when the `Router` /// itself is deallocated. If the interactor does not deallocate in time, a runtime assert is triggered, along with /// critical logging. +@MainActor public class LeakDetector { /// The singleton instance. diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index bafdccd..38e9b98 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -17,6 +17,7 @@ import Foundation /// Base class of an `Interactor` that actually has an associated `Presenter` and `View`. +@MainActor open class PresentableInteractor: Interactor { /// The `Presenter` associated with this `Interactor`. @@ -33,7 +34,7 @@ open class PresentableInteractor: Interactor { // MARK: - Private - deinit { + isolated deinit { LeakDetector.instance.expectDeallocate(object: presenter as AnyObject) } } diff --git a/RIBs/Classes/Presenter.swift b/RIBs/Classes/Presenter.swift index edc1d4f..bf7a5f4 100644 --- a/RIBs/Classes/Presenter.swift +++ b/RIBs/Classes/Presenter.swift @@ -17,11 +17,13 @@ import Foundation /// The base protocol for all `Presenter`s. +@MainActor public protocol Presentable: AnyObject {} /// The base class of all `Presenter`s. A `Presenter` translates business models into values the corresponding /// `ViewController` can consume and display. It also maps UI events to business logic method, invoked to /// its listener. +@MainActor open class Presenter: Presentable { /// The view controller of this presenter. diff --git a/RIBs/Classes/Router.swift b/RIBs/Classes/Router.swift index de593d0..1371702 100644 --- a/RIBs/Classes/Router.swift +++ b/RIBs/Classes/Router.swift @@ -17,6 +17,7 @@ import RxSwift /// The lifecycle stages of a router scope. +@MainActor public enum RouterLifecycle { /// Router did load. @@ -24,6 +25,7 @@ public enum RouterLifecycle { } /// The scope of a `Router`, defining various lifecycles of a `Router`. +@MainActor public protocol RouterScope: AnyObject { /// An observable that emits values when the router scope reaches its corresponding life-cycle stages. This @@ -32,6 +34,7 @@ public protocol RouterScope: AnyObject { } /// The base protocol for all routers. +@MainActor public protocol Routing: RouterScope { // The following methods must be declared in the base protocol, since `Router` internally invokes these methods. @@ -73,6 +76,7 @@ public protocol Routing: RouterScope { /// Router drives the lifecycle of its owned `Interactor`. /// /// Routers should always use helper builders to instantiate children routers. +@MainActor open class Router: Routing { /// The corresponding `Interactor` owned by this `Router`. @@ -210,18 +214,19 @@ open class Router: Routing { detachChild(child) } } - - deinit { + + isolated deinit { interactable.deactivate() - + if !children.isEmpty { detachAllChildren() } - + lifecycleSubject.onCompleted() - + deinitDisposable.dispose() - + LeakDetector.instance.expectDeallocate(object: interactable) + } } diff --git a/RIBs/Classes/ViewControllable.swift b/RIBs/Classes/ViewControllable.swift index 0e27040..211cf31 100644 --- a/RIBs/Classes/ViewControllable.swift +++ b/RIBs/Classes/ViewControllable.swift @@ -17,6 +17,7 @@ import UIKit /// Basic interface between a `Router` and the UIKit `UIViewController`. +@MainActor public protocol ViewControllable: AnyObject { var uiviewController: UIViewController { get } diff --git a/RIBs/Classes/ViewableRouter.swift b/RIBs/Classes/ViewableRouter.swift index 82488b4..e562cc3 100644 --- a/RIBs/Classes/ViewableRouter.swift +++ b/RIBs/Classes/ViewableRouter.swift @@ -17,6 +17,7 @@ import RxSwift /// The base protocol for all routers that own their own view controllers. +@MainActor public protocol ViewableRouting: Routing { // The following methods must be declared in the base protocol, since `Router` internally invokes these methods. @@ -33,6 +34,7 @@ public protocol ViewableRouting: Routing { /// A `Router` acts on inputs from its corresponding interactor, to manipulate application state and view state, /// forming a tree of routers that drives the tree of view controllers. Router drives the lifecycle of its owned /// interactor. `Router`s should always use helper builders to instantiate children `Router`s. +@MainActor open class ViewableRouter: Router, ViewableRouting { /// The corresponding `ViewController` owned by this `Router`. @@ -89,7 +91,7 @@ open class ViewableRouter: Router Disposable { if let compositeDisposable = worker.disposable { _ = compositeDisposable.insert(self) @@ -199,6 +201,7 @@ public extension Disposable { } } +@MainActor fileprivate class WeakInteractorScope: InteractorScope { weak var sourceScope: InteractorScope? diff --git a/RIBs/Classes/Workflow/Workflow.swift b/RIBs/Classes/Workflow/Workflow.swift index 472ffa3..40f1e2a 100644 --- a/RIBs/Classes/Workflow/Workflow.swift +++ b/RIBs/Classes/Workflow/Workflow.swift @@ -23,6 +23,7 @@ import RxSwift /// RIB. /// /// A workflow should always start at the root of the tree. +@MainActor open class Workflow { /// Called when the last step observable is completed. @@ -104,6 +105,7 @@ open class Workflow { /// steps. /// /// Steps are asynchronous by nature. +@MainActor open class Step { private let workflow: Workflow @@ -185,6 +187,7 @@ public extension ObservableType { /// - parameter workflow: The workflow this step belongs to. /// - returns: The newly forked step in the workflow. `nil` if this observable does not conform to the required /// generic type of (ActionableItemType, ValueType). + @MainActor func fork(_ workflow: Workflow) -> Step? { if let stepObservable = self as? Observable<(ActionableItemType, ValueType)> { workflow.didFork() @@ -206,6 +209,7 @@ public extension Disposable { /// - note: This is the preferred method when trying to confine a subscription to the lifecycle of a `Workflow`. /// /// - parameter workflow: The workflow to dispose the subscription with. + @MainActor func disposeWith(workflow: Workflow) { _ = workflow.compositeDisposable.insert(self) } @@ -220,6 +224,7 @@ public extension Disposable { /// /// - parameter workflow: The workflow to dispose the subscription with. @available(*, deprecated, renamed: "disposeWith(workflow:)") + @MainActor func disposeWith(worflow: Workflow) { disposeWith(workflow: worflow) } diff --git a/RIBsTests/ComponentizedBuilderTests.swift b/RIBsTests/ComponentizedBuilderTests.swift index d087729..af1e877 100644 --- a/RIBsTests/ComponentizedBuilderTests.swift +++ b/RIBsTests/ComponentizedBuilderTests.swift @@ -18,6 +18,7 @@ import XCTest import CwlPreconditionTesting +@MainActor class ComponentizedBuilderTests: XCTestCase { func test_componentForCurrentPass_builderReturnsSameInstance_verifyAssertion() { diff --git a/RIBsTests/DI/ComponentTests.swift b/RIBsTests/DI/ComponentTests.swift index 3e10878..b05a215 100644 --- a/RIBsTests/DI/ComponentTests.swift +++ b/RIBsTests/DI/ComponentTests.swift @@ -17,6 +17,7 @@ import XCTest @testable import RIBs +@MainActor final class ComponentTests: XCTestCase { // MARK: - Tests diff --git a/RIBsTests/Interactor/InteractorTests.swift b/RIBsTests/Interactor/InteractorTests.swift index 5797869..11e5f0a 100644 --- a/RIBsTests/Interactor/InteractorTests.swift +++ b/RIBsTests/Interactor/InteractorTests.swift @@ -9,6 +9,7 @@ import XCTest import RxSwift +@MainActor final class InteractorTests: XCTestCase { private var interactor: InteractorMock! @@ -87,7 +88,7 @@ final class InteractorTests: XCTestCase { XCTAssertEqual(interactor.willResignActiveCallCount, 1) } - func test_isActiveStream_completedOnInteractorDeinit() { + func test_isActiveStream_completedOnInteractorDeinit() async { // given var isActiveStreamCompleted = false interactor.activate() @@ -99,7 +100,6 @@ final class InteractorTests: XCTestCase { interactor = nil // then XCTAssertTrue(isActiveStreamCompleted) - } // MARK: - BEGIN Observables Attached/Detached to/from Interactor @@ -131,7 +131,7 @@ final class InteractorTests: XCTestCase { XCTAssertTrue(onDisposeCalled) } - func test_observableIsDisposedOnInteractorDeinit() { + func test_observableIsDisposedOnInteractorDeinit() async { // given var onDisposeCalled = false let subjectEmiitingValues: PublishSubject = .init() diff --git a/RIBsTests/Interactor/PresentableInteractorTests.swift b/RIBsTests/Interactor/PresentableInteractorTests.swift index 7505703..76bac6f 100644 --- a/RIBsTests/Interactor/PresentableInteractorTests.swift +++ b/RIBsTests/Interactor/PresentableInteractorTests.swift @@ -13,6 +13,7 @@ protocol TestPresenter {} final class PresenterMock: TestPresenter {} +@MainActor final class PresentableInteractorTests: XCTestCase { private var interactor: PresentableInteractor! @@ -22,7 +23,7 @@ final class PresentableInteractorTests: XCTestCase { } - func test_deinit_doesNotLeakPresenter() { + func test_deinit_doesNotLeakPresenter() async { // given let presenterMock = PresenterMock() let disposeBag = DisposeBag() diff --git a/RIBsTests/LaunchRouterTests.swift b/RIBsTests/LaunchRouterTests.swift index dba33ae..c8d311d 100644 --- a/RIBsTests/LaunchRouterTests.swift +++ b/RIBsTests/LaunchRouterTests.swift @@ -17,6 +17,7 @@ @testable import RIBs import XCTest +@MainActor final class LaunchRouterTests: XCTestCase { private var launchRouter: LaunchRouting! diff --git a/RIBsTests/MultiStageComponentizedBuilderTests.swift b/RIBsTests/MultiStageComponentizedBuilderTests.swift index 917005f..c23a9de 100644 --- a/RIBsTests/MultiStageComponentizedBuilderTests.swift +++ b/RIBsTests/MultiStageComponentizedBuilderTests.swift @@ -18,6 +18,7 @@ import XCTest import CwlPreconditionTesting +@MainActor class MultiStageComponentizedBuilderTests: XCTestCase { private var builder: MockMultiStageComponentizedBuilder! diff --git a/RIBsTests/Router/RouterTests.swift b/RIBsTests/Router/RouterTests.swift index 2ab8a20..f99c288 100644 --- a/RIBsTests/Router/RouterTests.swift +++ b/RIBsTests/Router/RouterTests.swift @@ -49,6 +49,7 @@ final class RouterMock: Routing { } } +@MainActor final class RouterTests: XCTestCase { private var router: Router! @@ -73,7 +74,7 @@ final class RouterTests: XCTestCase { // MARK: - Tests - func test_load_verifyLifecycleObservable() { + func test_load_verifyLifecycleObservable() async { router = Router(interactor: InteractableMock()) var currentLifecycle: RouterLifecycle? var didComplete = false @@ -148,7 +149,7 @@ final class RouterTests: XCTestCase { XCTAssertEqual(mockChildInteractor.deactivateCallCount, 1) } - func test_detachChild_deactivatesSubtreeOfTheChild() { + func test_detachChild_deactivatesSubtreeOfTheChild() async { // given router = Router(interactor: InteractableMock()) let childInteractor = Interactor() @@ -167,7 +168,7 @@ final class RouterTests: XCTestCase { XCTAssertEqual(grandChildInteractor.deactivateCallCount, 1) } - func test_deinit_triggers_leakDetection() { + func test_deinit_triggers_leakDetection() async { // given let interactor = InteractableMock() router = Router(interactor: interactor) diff --git a/RIBsTests/Router/ViewableRouterTests.swift b/RIBsTests/Router/ViewableRouterTests.swift index 88d3f0e..36ea8f0 100644 --- a/RIBsTests/Router/ViewableRouterTests.swift +++ b/RIBsTests/Router/ViewableRouterTests.swift @@ -18,6 +18,7 @@ final class ViewControllerMock: ViewControllable { } } +@MainActor final class ViewableRouterTests: XCTestCase { private var router: ViewableRouter, ViewControllerMock>! @@ -42,7 +43,7 @@ final class ViewableRouterTests: XCTestCase { XCTAssertEqual(leakDetectorMock.expectViewControllerDisappearCallCount, 1) } - func test_deinit_triggers_leakDetection() { + func test_deinit_triggers_leakDetection() async { // given let interactor = PresentableInteractor(presenter: PresenterMock()) let viewController = ViewControllerMock() diff --git a/RIBsTests/Worker/WorkerTests.swift b/RIBsTests/Worker/WorkerTests.swift index ed86e10..5ac2d53 100644 --- a/RIBsTests/Worker/WorkerTests.swift +++ b/RIBsTests/Worker/WorkerTests.swift @@ -18,6 +18,7 @@ import XCTest import RxSwift @testable import RIBs +@MainActor final class WorkerTests: XCTestCase { private var worker: TestWorker! diff --git a/RIBsTests/Workflow/WorkflowTests.swift b/RIBsTests/Workflow/WorkflowTests.swift index ccfb812..925f9e7 100644 --- a/RIBsTests/Workflow/WorkflowTests.swift +++ b/RIBsTests/Workflow/WorkflowTests.swift @@ -18,6 +18,7 @@ import XCTest import RxSwift @testable import RIBs +@MainActor final class WorkerflowTests: XCTestCase { func test_nestedStepsDoNotRepeat() { diff --git a/SWIFT6_STRICT_CONCURRENCY_MIGRATION.md b/SWIFT6_STRICT_CONCURRENCY_MIGRATION.md new file mode 100644 index 0000000..59e983d --- /dev/null +++ b/SWIFT6_STRICT_CONCURRENCY_MIGRATION.md @@ -0,0 +1,138 @@ +# Migrating to Swift 6 Strict Concurrency with RIBs + +This guide covers what you need to do (if anything) when adopting Swift 6 and/or stricter concurrency settings in a project that uses RIBs. + +## Context + +The RIBs framework has always operated on the main thread at runtime. This release makes that explicit at the type system level by annotating all core framework types with `@MainActor`. For most existing projects this is a transparent, non-breaking change. For projects moving to Swift 6, the path depends on your default isolation setting. + +## Requirements + +`isolated deinit` — used in `Interactor`, `PresentableInteractor`, `Router`, `ViewableRouter`, and `Worker` — requires **Xcode 26.2 / Swift 6.2**. The rest of the `@MainActor` annotations are compatible with earlier toolchains. Apple requires apps submitted to the App Store to be built with Xcode 26 starting April 28, 2026 ([Apple's developer news](https://developer.apple.com/news/?id=ueeok6yw), more details [here](https://developer.apple.com/app-store/submitting/)). + +## Compatibility at a glance + +| Swift | Default isolation | Strictness | What you need to do | +|---|---|---|---| +| 5 | nonisolated | Minimal | Nothing | +| 5 | nonisolated | Targeted | Nothing | +| 5 | nonisolated | Complete | Nothing | +| 5 | `@MainActor` | Minimal | Nothing | +| 5 | `@MainActor` | Targeted | Nothing | +| 5 | `@MainActor` | Complete | Nothing | +| 6 | nonisolated | Minimal | ❌ Not supported — see below | +| 6 | nonisolated | Targeted | ❌ Not supported — see below | +| 6 | nonisolated | Complete | ❌ Not supported — see below | +| 6 | `@MainActor` | Minimal | Switch to `@MainActor` default + handle RxSwift caveat | +| 6 | `@MainActor` | Targeted | Switch to `@MainActor` default + handle RxSwift caveat | +| 6 | `@MainActor` | Complete | Switch to `@MainActor` default + handle RxSwift caveat | + +--- + +## Swift 5 (all configurations) + +No action required. `@MainActor` annotations on library types are additive and fully source-compatible. Your existing RIB subclasses compile and behave identically. + +--- + +## Swift 6 + `nonisolated` default (not supported) + +When your project uses Swift 6 with `nonisolated` as the default isolation, your own types are implicitly `nonisolated`. Subclassing or conforming to `@MainActor`-annotated framework types produces actor isolation mismatch compile errors throughout your RIB code. This configuration is not supported. + +**Your options:** + +1. **Stay on Swift 5** — fully supported in all configurations, no code changes needed. +2. **Switch to Swift 6 with `@MainActor` default isolation** — the supported path for Swift 6 users (see next section). +3. **Stay on Swift 6 with `nonisolated` default and add explicit `@MainActor` annotations throughout your RIB code** — if switching your entire project's default isolation is not feasible, you can keep `nonisolated` as the default and manually annotate your code to satisfy the compiler. This goes beyond just annotating RIB subclasses — you will also need to annotate the protocols your app defines (presentable listener protocols, interactor listener protocols, routing protocols, etc.) and potentially other types that interact with RIBs at isolation boundaries. The exact scope of changes depends on your codebase. This path is possible but is left to you to work through; the compiler will guide you to every site that needs attention. + +--- + +## Swift 6 + `@MainActor` default isolation + +This is the target configuration. With `@MainActor` as your project's default isolation, your own types are also implicitly `@MainActor`, aligning with the framework. This is how brand new projects with Xcode 26+ are set up by default. + +### Enabling it + +In your Xcode project's build settings: + +- **Swift Language Version:** Swift 6 +- **Swift Compiler — Upcoming Features / Strict Concurrency:** your choice of Minimal, Targeted, or Complete — all work +- **Default Actor Isolation:** `@MainActor` + (`-default-isolation MainActor` in `OTHER_SWIFT_FLAGS` if setting manually) + +### Your RIB subclasses + +Your custom `Interactor`, `Router`, `Builder`, `Worker`, and `Presenter` subclasses inherit `@MainActor` isolation through the base classes. No annotation needed in most cases. + +### Services and injected dependencies + +Anything passed into a RIB via constructor injection through a `Component` must be compatible with `@MainActor`: + +- Types that are themselves `@MainActor` — no changes needed +- Types that are `Sendable` — no changes needed +- Types that do background work — mark them `nonisolated` where appropriate, or use `async`/`await` to cross actor boundaries explicitly + +### `deinit` in your own RIB subclasses + +If you have custom `deinit` implementations that access `@MainActor`-isolated state, mark them `isolated deinit` (Xcode 26.2 / Swift 6.2 required): + +```swift +final class MyInteractor: PresentableInteractor { + isolated deinit { + // safe to access @MainActor state here + someMainActorResource.cleanup() + } +} +``` + +If you are on an earlier toolchain temporarily, `nonisolated(unsafe)` is a stopgap, but migrate to `isolated deinit` as soon as your toolchain supports it. + +--- + +## RxSwift `@Sendable` caveat (Swift 6 only) + +With Swift 6 enabled, closures passed to RxSwift operators (`map`, `filter`, `flatMap`, `subscribe`, etc.) must be `@Sendable` or you will encounter a runtime crash. This is a known RxSwift limitation ([ReactiveX/RxSwift#2639](https://github.com/ReactiveX/RxSwift/pull/2639)) that predates and is independent of these RIBs changes. + +**Option A — annotate affected closures:** + +```swift +observable + .map { @Sendable value in transform(value) } + .subscribe(onNext: { @Sendable value in handle(value) }) +``` + +**Option B — migrate to async/await:** + +RIBs now fully supports async/await at the type system level. The standard bridging pattern for one-shot async work is: + +```swift +Single.create { single in + Task { + do { + let result = try await myAsyncFunction() + single(.success(result)) + } catch { + single(.failure(error)) + } + } + return Disposables.create() +} +.observe(on: MainScheduler.instance) +.subscribe(onSuccess: { [weak self] result in + self?.handle(result) +}) +.disposeOnDeactivate(interactor: self) +``` + +Additional async/await convenience utilities are planned as a follow-up release, making this pattern even more concise. + +--- + +## Summary + +| Scenario | Action | +|---|---| +| Staying on Swift 5 | Nothing — fully compatible | +| Moving to Swift 6, keeping `nonisolated` default | Not supported without changes; annotate each RIB subclass explicitly with `@MainActor`, or switch to `@MainActor` default | +| Moving to Swift 6, switching to `@MainActor` default | Enable `@MainActor` default isolation; handle RxSwift `@Sendable` if using RxSwift | +| Custom `deinit` accessing main-actor state | Use `isolated deinit` (Xcode 26.2+) |