From 837069904de6088e6e87cc6420c84ed66ac25161 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 16 Aug 2025 20:10:40 -0500 Subject: [PATCH 01/30] Bump minimum iOS version requirement to iOS 15. Bump RxSwift dependency version to 6.9.0 --- Example/RIBs.xcodeproj/project.pbxproj | 2 ++ Package.swift | 11 +++++++---- RIBs.xcodeproj/project.pbxproj | 4 ++-- .../tutorial1/TicTacToe.xcodeproj/project.pbxproj | 2 ++ .../tutorial2/TicTacToe.xcodeproj/project.pbxproj | 2 ++ .../TicTacToe.xcodeproj/project.pbxproj | 2 ++ .../tutorial3/TicTacToe.xcodeproj/project.pbxproj | 2 ++ .../TicTacToe.xcodeproj/project.pbxproj | 2 ++ .../tutorial4/TicTacToe.xcodeproj/project.pbxproj | 2 ++ 9 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Example/RIBs.xcodeproj/project.pbxproj b/Example/RIBs.xcodeproj/project.pbxproj index e810a05..e361f11 100644 --- a/Example/RIBs.xcodeproj/project.pbxproj +++ b/Example/RIBs.xcodeproj/project.pbxproj @@ -373,6 +373,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = RIBs/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; @@ -388,6 +389,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = RIBs/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; diff --git a/Package.swift b/Package.swift index b49d031..2ee5ba3 100644 --- a/Package.swift +++ b/Package.swift @@ -1,22 +1,25 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.5 import PackageDescription let package = Package( name: "RIBs", platforms: [ - .iOS(.v9), + .iOS("15.0"), ], products: [ .library(name: "RIBs", targets: ["RIBs"]), ], dependencies: [ - .package(url: "https://github.com/ReactiveX/RxSwift", from: "6.5.0"), + .package(url: "https://github.com/ReactiveX/RxSwift", from: "6.9.0"), .package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", from: "2.2.2"), // for testTarget only ], targets: [ .target( name: "RIBs", - dependencies: ["RxSwift", "RxRelay"], + dependencies: [ + .product(name: "RxSwift", package: "RxSwift"), + .product(name: "RxRelay", package: "RxSwift") + ], path: "RIBs" ), .testTarget( diff --git a/RIBs.xcodeproj/project.pbxproj b/RIBs.xcodeproj/project.pbxproj index b5ae108..049e88a 100644 --- a/RIBs.xcodeproj/project.pbxproj +++ b/RIBs.xcodeproj/project.pbxproj @@ -560,7 +560,7 @@ ); INFOPLIST_FILE = RIBs/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.uber.RIBs; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; @@ -587,7 +587,7 @@ ); INFOPLIST_FILE = RIBs/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.uber.RIBs; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; diff --git a/tutorials/tutorial1/TicTacToe.xcodeproj/project.pbxproj b/tutorials/tutorial1/TicTacToe.xcodeproj/project.pbxproj index 0c8b1ff..4de8c28 100644 --- a/tutorials/tutorial1/TicTacToe.xcodeproj/project.pbxproj +++ b/tutorials/tutorial1/TicTacToe.xcodeproj/project.pbxproj @@ -384,6 +384,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -398,6 +399,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/tutorials/tutorial2/TicTacToe.xcodeproj/project.pbxproj b/tutorials/tutorial2/TicTacToe.xcodeproj/project.pbxproj index 0f04442..cfef53a 100644 --- a/tutorials/tutorial2/TicTacToe.xcodeproj/project.pbxproj +++ b/tutorials/tutorial2/TicTacToe.xcodeproj/project.pbxproj @@ -569,6 +569,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -583,6 +584,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/tutorials/tutorial3-completed/TicTacToe.xcodeproj/project.pbxproj b/tutorials/tutorial3-completed/TicTacToe.xcodeproj/project.pbxproj index b432bd0..c0ed3d1 100644 --- a/tutorials/tutorial3-completed/TicTacToe.xcodeproj/project.pbxproj +++ b/tutorials/tutorial3-completed/TicTacToe.xcodeproj/project.pbxproj @@ -488,6 +488,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -502,6 +503,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/tutorials/tutorial3/TicTacToe.xcodeproj/project.pbxproj b/tutorials/tutorial3/TicTacToe.xcodeproj/project.pbxproj index 6e9d0e1..110f276 100644 --- a/tutorials/tutorial3/TicTacToe.xcodeproj/project.pbxproj +++ b/tutorials/tutorial3/TicTacToe.xcodeproj/project.pbxproj @@ -476,6 +476,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -490,6 +491,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/tutorials/tutorial4-completed/TicTacToe.xcodeproj/project.pbxproj b/tutorials/tutorial4-completed/TicTacToe.xcodeproj/project.pbxproj index 9c9f2d5..ab67949 100644 --- a/tutorials/tutorial4-completed/TicTacToe.xcodeproj/project.pbxproj +++ b/tutorials/tutorial4-completed/TicTacToe.xcodeproj/project.pbxproj @@ -584,6 +584,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -598,6 +599,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/tutorials/tutorial4/TicTacToe.xcodeproj/project.pbxproj b/tutorials/tutorial4/TicTacToe.xcodeproj/project.pbxproj index 5de64de..3cd8cfb 100644 --- a/tutorials/tutorial4/TicTacToe.xcodeproj/project.pbxproj +++ b/tutorials/tutorial4/TicTacToe.xcodeproj/project.pbxproj @@ -580,6 +580,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -594,6 +595,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TicTacToe/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ubercab.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; From 55074126ab5a861967d7ed704140066df8e8cd21 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 16 Aug 2025 20:18:08 -0500 Subject: [PATCH 02/30] Bump iOS version in cocoapods --- RIBs.podspec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RIBs.podspec b/RIBs.podspec index b443369..fb0a69b 100644 --- a/RIBs.podspec +++ b/RIBs.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'RIBs' - s.version = '0.9.3' + s.version = '1.0.0' s.summary = 'Uber\'s cross-platform mobile architecture.' s.description = <<-DESC RIBs is the cross-platform architecture behind many mobile apps at Uber. This architecture framework is designed for mobile apps with a large number of engineers and nested states. @@ -8,12 +8,12 @@ RIBs is the cross-platform architecture behind many mobile apps at Uber. This ar s.homepage = 'https://github.com/uber/RIBs-iOS' s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE.txt' } s.author = { 'uber' => 'mobile-open-source@uber.com' } - s.source = { :git => 'https://github.com/uber/RIBs-iOS.git', :tag => 'v' + s.version.to_s } - s.ios.deployment_target = '9.0' + s.source = { :git => 'https://github.com/uber/RIBs-iOS.git', :tag => s.version.to_s } + s.ios.deployment_target = '15.0' s.swift_version = '5.0' s.source_files = 'RIBs/Classes/**/*' - s.dependency 'RxSwift', '~> 6.5.0' - s.dependency 'RxRelay', '~> 6.5.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' From b565c3005e2a3593f2aa42d69bfd0cb61beea3be Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 30 Aug 2025 11:40:44 -0500 Subject: [PATCH 03/30] Fix tests in CI --- Example/.ruby-version | 1 + Example/Gemfile | 5 ++ Example/Gemfile.lock | 118 +++++++++++++++++++++++++ Example/Podfile | 2 +- Example/RIBs.xcodeproj/project.pbxproj | 4 + 5 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 Example/.ruby-version create mode 100644 Example/Gemfile create mode 100644 Example/Gemfile.lock diff --git a/Example/.ruby-version b/Example/.ruby-version new file mode 100644 index 0000000..fd2a018 --- /dev/null +++ b/Example/.ruby-version @@ -0,0 +1 @@ +3.1.0 diff --git a/Example/Gemfile b/Example/Gemfile new file mode 100644 index 0000000..a55e88c --- /dev/null +++ b/Example/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem 'cocoapods' diff --git a/Example/Gemfile.lock b/Example/Gemfile.lock new file mode 100644 index 0000000..ab81a50 --- /dev/null +++ b/Example/Gemfile.lock @@ -0,0 +1,118 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + activesupport (7.2.2.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + atomos (0.1.3) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.2.2) + claide (1.1.0) + cocoapods (1.16.2) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.16.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored2 (3.1.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) + drb (2.2.3) + escape (0.0.4) + ethon (0.15.0) + ffi (>= 1.15.0) + ffi (1.17.2) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + httpclient (2.9.0) + mutex_m + i18n (1.14.7) + concurrent-ruby (~> 1.0) + json (2.13.2) + logger (1.7.0) + minitest (5.25.5) + molinillo (0.8.0) + mutex_m (0.3.0) + nanaimo (0.4.0) + nap (1.1.0) + netrc (0.11.0) + nkf (0.2.0) + public_suffix (4.0.7) + rexml (3.4.2) + ruby-macho (2.5.1) + securerandom (0.4.1) + typhoeus (1.5.0) + ethon (>= 0.9.0, < 0.16.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + +PLATFORMS + arm64-darwin + ruby + x86_64-darwin + +DEPENDENCIES + cocoapods + +BUNDLED WITH + 2.6.9 diff --git a/Example/Podfile b/Example/Podfile index 5faa4fd..6c660d9 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,6 +1,6 @@ use_frameworks! -platform :ios, '10.0' +platform :ios, '15.0' target 'RIBs_Example' do pod 'RIBs', :path => '../', :testspecs => ['Tests'] diff --git a/Example/RIBs.xcodeproj/project.pbxproj b/Example/RIBs.xcodeproj/project.pbxproj index e361f11..45519b9 100644 --- a/Example/RIBs.xcodeproj/project.pbxproj +++ b/Example/RIBs.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; + 7EAB71912E635DC2003F72CC /* Podfile in Resources */ = {isa = PBXBuildFile; fileRef = 7EAB71902E635DC2003F72CC /* Podfile */; }; F72111BE3D6D8AF7B540B2D5 /* Pods_RIBs_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 904C7089FA30546DB519E5B6 /* Pods_RIBs_Example.framework */; }; /* End PBXBuildFile section */ @@ -26,6 +27,7 @@ 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 79412201A539E9C0ACDDCA67 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; + 7EAB71902E635DC2003F72CC /* Podfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Podfile; sourceTree = ""; }; 8352C4F16B235B2430F102DC /* Pods-RIBs_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RIBs_Example.release.xcconfig"; path = "Target Support Files/Pods-RIBs_Example/Pods-RIBs_Example.release.xcconfig"; sourceTree = ""; }; 84C807AE5AA5710BFA3EE5FC /* Pods-RIBs_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RIBs_Tests.release.xcconfig"; path = "Target Support Files/Pods-RIBs_Tests/Pods-RIBs_Tests.release.xcconfig"; sourceTree = ""; }; 904C7089FA30546DB519E5B6 /* Pods_RIBs_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RIBs_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -49,6 +51,7 @@ 607FACC71AFB9204008FA782 = { isa = PBXGroup; children = ( + 7EAB71902E635DC2003F72CC /* Podfile */, 607FACF51AFB993E008FA782 /* Podspec Metadata */, 607FACD21AFB9204008FA782 /* Example for RIBs */, 607FACD11AFB9204008FA782 /* Products */, @@ -181,6 +184,7 @@ files = ( 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, + 7EAB71912E635DC2003F72CC /* Podfile in Resources */, 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; From 5d269e928455fd31489514672e4e412a1252dcd3 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 30 Aug 2025 12:17:15 -0500 Subject: [PATCH 04/30] Lock in the RxSwift dependency to 6.x.x --- Package.swift | 2 +- RIBs.podspec | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 2ee5ba3..53d3f9b 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( .library(name: "RIBs", targets: ["RIBs"]), ], dependencies: [ - .package(url: "https://github.com/ReactiveX/RxSwift", from: "6.9.0"), + .package(url: "https://github.com/ReactiveX/RxSwift", "6.9.0"..<"7.0.0"), .package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", from: "2.2.2"), // for testTarget only ], targets: [ diff --git a/RIBs.podspec b/RIBs.podspec index fb0a69b..38f8ecd 100644 --- a/RIBs.podspec +++ b/RIBs.podspec @@ -12,9 +12,9 @@ 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.9.0' - s.dependency 'RxRelay', '~> 6.9.0' - + s.dependency 'RxSwift', '~> 6.0' + s.dependency 'RxRelay', '~> 6.0' + s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'RIBsTests/**/*.swift' test_spec.dependency 'CwlPreconditionTesting' From 3a4624dec48ed963bb1c666cc54ed4ec8e970da5 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 30 Aug 2025 12:21:00 -0500 Subject: [PATCH 05/30] Fixing CI --- .github/workflows/iOS.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/iOS.yml b/.github/workflows/iOS.yml index 653178e..d0929ad 100644 --- a/.github/workflows/iOS.yml +++ b/.github/workflows/iOS.yml @@ -29,7 +29,7 @@ jobs: -workspace RIBs.xcworkspace \ -scheme RIBs-Example \ -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' \ + -destination 'platform=iOS Simulator,name=iPhone 16' \ -enableCodeCoverage YES \ clean test From 4e8a08d115451b15ec6ba9f657b134be192ab1bb Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 30 Aug 2025 12:33:04 -0500 Subject: [PATCH 06/30] Version 1.0.0 --- CHANGELOG.md | 5 +++++ README.md | 4 ++-- RIBs/Info.plist | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f55f8eb..cf34102 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,3 +136,8 @@ * Increase buffer capacity for mutableRouterEvents flow within RibEvents by @RahulDMello in https://github.com/uber/RIBs/pull/635 * Add test asserting Rx subscription is disposed after `RibCoroutineWor… by @psteiger in https://github.com/uber/RIBs/pull/628 +### Version 1.0.0 + +* Bumps RxSwift dependency version to 6.x.x (6.9.0 at the time of the release) by @alexvbush +* Adds Swift Package Manager (SPM) setup by @alexvbush +* Improves CocoaPods and Carthage setup by @alexvbush \ No newline at end of file diff --git a/README.md b/README.md index 8cfc107..5534153 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ For usage of the tooling built around RIBs, please see the [Tooling section](htt To integrate RIBs into your project add the following to your `Podfile`: ```ruby -pod 'RIBs', '~> 0.9' +pod 'RIBs', '~> 1.0' ``` #### Carthage @@ -58,7 +58,7 @@ pod 'RIBs', '~> 0.9' To integrate RIBs into your project using Carthage add the following to your `Cartfile`: ```ruby -github "uber/RIBs" ~> 0.9 +github "uber/RIBs" ~> 1.0 ``` ## Related projects diff --git a/RIBs/Info.plist b/RIBs/Info.plist index 992c476..4c0d218 100644 --- a/RIBs/Info.plist +++ b/RIBs/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.9.3 + 1.0.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass From 0bed2237e7929bb77219f79db096ac9c92fe380a Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 30 Aug 2025 12:41:06 -0500 Subject: [PATCH 07/30] Update readme --- README.md | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5534153..2372f11 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,25 @@ There are some other novel things about RIBs. However, these could also be imple For usage of the tooling built around RIBs, please see the [Tooling section](https://github.com/uber/RIBs/wiki#rib-tooling) in our documentation. ## Installation for iOS -#### CocoaPods + +### Swift Package Manager (Recommended) + +To integrate RIBs into your project using Swift Package Manager: + +1. In Xcode, go to **File** → **Add Package Dependencies** +2. Enter the repository URL: `https://github.com/uber/RIBs-iOS.git` +3. Select the version constraint: `~> 1.0` +4. Click **Add Package** + +Alternatively, you can add it to your `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/uber/RIBs-iOS.git", from: "1.0.0") +] +``` + +### CocoaPods To integrate RIBs into your project add the following to your `Podfile`: @@ -53,7 +71,7 @@ To integrate RIBs into your project add the following to your `Podfile`: pod 'RIBs', '~> 1.0' ``` -#### Carthage +### Carthage To integrate RIBs into your project using Carthage add the following to your `Cartfile`: @@ -61,9 +79,24 @@ To integrate RIBs into your project using Carthage add the following to your `Ca github "uber/RIBs" ~> 1.0 ``` +## Dependencies + +When you integrate RIBs into your project, it will automatically bring the following dependencies: + +### Core Dependencies +- **RxSwift** (~> 6.0) - Reactive programming library for Swift +- **RxRelay** (~> 6.0) - Reactive relays for state management + +### Platform Requirements +- **iOS 15.0+** - Minimum deployment target +- **Swift 5.0+** - Required Swift version + +These dependencies are automatically managed by your chosen package manager and will be resolved to compatible versions. + ## Related projects If you like RIBs, check out other related open source projects from our team: +- [RIBs-Android](https://github.com/uber/RIBs): Android version of RIBs framework implementation - [Needle](https://github.com/uber/needle): a compile-time safe Swift dependency injection framework. - [Motif](https://github.com/uber/motif): An abstract on top of Dagger offering simpler APIs for nested scopes. - [Swift Concurrency](https://github.com/uber/swift-concurrency): a set of concurrency utility classes used by Uber, inspired by the equivalent [java.util.concurrent](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html) package classes. From afa08eb1d248d839dde6efe8e9718d59fecd3c0e Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 16 Aug 2025 20:10:40 -0500 Subject: [PATCH 08/30] Bump minimum iOS version requirement to iOS 15. Bump RxSwift dependency version to 6.9.0 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 53d3f9b..2ee5ba3 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( .library(name: "RIBs", targets: ["RIBs"]), ], dependencies: [ - .package(url: "https://github.com/ReactiveX/RxSwift", "6.9.0"..<"7.0.0"), + .package(url: "https://github.com/ReactiveX/RxSwift", from: "6.9.0"), .package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", from: "2.2.2"), // for testTarget only ], targets: [ From 48d16f64109b65480e0c99ea22f67a3ac1e290d2 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 16 Aug 2025 20:18:08 -0500 Subject: [PATCH 09/30] Bump iOS version in cocoapods --- RIBs.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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' From 78950a16d747b27e644e1767e29fec8f45e3bcab Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 30 Aug 2025 12:17:15 -0500 Subject: [PATCH 10/30] Lock in the RxSwift dependency to 6.x.x --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 2ee5ba3..53d3f9b 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( .library(name: "RIBs", targets: ["RIBs"]), ], dependencies: [ - .package(url: "https://github.com/ReactiveX/RxSwift", from: "6.9.0"), + .package(url: "https://github.com/ReactiveX/RxSwift", "6.9.0"..<"7.0.0"), .package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", from: "2.2.2"), // for testTarget only ], targets: [ From 30f4b84dff8e85c6f16e3b10ec0787bf09354d02 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 10 Jan 2026 13:06:16 -0600 Subject: [PATCH 11/30] Add a new small example ribs app --- {Example => Examples/Example1}/.ruby-version | 0 {Example => Examples/Example1}/Gemfile | 0 {Example => Examples/Example1}/Gemfile.lock | 0 {Example => Examples/Example1}/Podfile | 0 .../Example1}/RIBs.xcodeproj/project.pbxproj | 0 .../xcschemes/RIBs-Example.xcscheme | 0 .../Example1}/RIBs/AppDelegate.swift | 0 .../RIBs/Base.lproj/LaunchScreen.xib | 0 .../Example1}/RIBs/Base.lproj/Main.storyboard | 0 .../AppIcon.appiconset/Contents.json | 0 .../Example1}/RIBs/Info.plist | 0 .../Example1}/RIBs/ViewController.swift | 0 .../RIBsAppExample2.xcodeproj/project.pbxproj | 487 ++++++++++++++++++ .../RIBsAppExample2/AppComponent.swift | 9 + .../RIBsAppExample2/AppDelegate.swift | 36 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 24 + .../FirstViewableRIBBuilder.swift | 41 ++ .../FirstViewableRIBInteractor.swift | 54 ++ .../FirstViewableRIBRouter.swift | 49 ++ .../FirstViewableRIBViewController.swift | 27 + .../RIBsAppExample2/Info.plist | 25 + .../RIBsAppExample2/Root/RootBuilder.swift | 40 ++ .../RIBsAppExample2/Root/RootInteractor.swift | 47 ++ .../RIBsAppExample2/Root/RootRouter.swift | 47 ++ .../Root/RootViewController.swift | 44 ++ .../RIBsAppExample2/SceneDelegate.swift | 31 ++ .../SecondViewableRIBBuilder.swift | 39 ++ .../SecondViewableRIBInteractor.swift | 45 ++ .../SecondViewableRIBRouter.swift | 30 ++ .../SecondViewableRIBViewController.swift | 27 + .../RIBsAppExample2/ViewController.swift | 19 + .../RIBsAppExample2Tests.swift | 36 ++ 36 files changed, 1234 insertions(+) rename {Example => Examples/Example1}/.ruby-version (100%) rename {Example => Examples/Example1}/Gemfile (100%) rename {Example => Examples/Example1}/Gemfile.lock (100%) rename {Example => Examples/Example1}/Podfile (100%) rename {Example => Examples/Example1}/RIBs.xcodeproj/project.pbxproj (100%) rename {Example => Examples/Example1}/RIBs.xcodeproj/xcshareddata/xcschemes/RIBs-Example.xcscheme (100%) rename {Example => Examples/Example1}/RIBs/AppDelegate.swift (100%) rename {Example => Examples/Example1}/RIBs/Base.lproj/LaunchScreen.xib (100%) rename {Example => Examples/Example1}/RIBs/Base.lproj/Main.storyboard (100%) rename {Example => Examples/Example1}/RIBs/Images.xcassets/AppIcon.appiconset/Contents.json (100%) rename {Example => Examples/Example1}/RIBs/Info.plist (100%) rename {Example => Examples/Example1}/RIBs/ViewController.swift (100%) create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/AppComponent.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/AppDelegate.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Assets.xcassets/Contents.json create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Base.lproj/LaunchScreen.storyboard create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Base.lproj/Main.storyboard create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBViewController.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Info.plist create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Root/RootInteractor.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Root/RootRouter.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Root/RootViewController.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/SceneDelegate.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBRouter.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBViewController.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/ViewController.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2Tests/RIBsAppExample2Tests.swift 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/Example/Podfile b/Examples/Example1/Podfile similarity index 100% rename from Example/Podfile rename to Examples/Example1/Podfile 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..64bcd00 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj @@ -0,0 +1,487 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 7EB78D4F2F12CC0000547345 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EB78D4E2F12CC0000547345 /* 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 = ( + 7EB78D4F2F12CC0000547345 /* 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 */, + ); + 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 = ( + 7EB78D4D2F12CC0000547345 /* 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; + PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.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; + PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.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 */ + 7EB78D4D2F12CC0000547345 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../../RIBs-iOS"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 7EB78D4E2F12CC0000547345 /* 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/FirstViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift new file mode 100644 index 0000000..72f5702 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift @@ -0,0 +1,41 @@ +// +// 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 { + + var secondViewableRIBBuilder: SecondViewableRIBBuildable { + SecondViewableRIBBuilder(dependency: self) + } +} + +// MARK: - Builder + +protocol FirstViewableRIBBuildable: Buildable { + func build(withListener listener: FirstViewableRIBListener) -> FirstViewableRIBRouting +} + +final class FirstViewableRIBBuilder: Builder, FirstViewableRIBBuildable { + + override init(dependency: FirstViewableRIBDependency) { + super.init(dependency: dependency) + } + + func build(withListener listener: FirstViewableRIBListener) -> FirstViewableRIBRouting { + let component = FirstViewableRIBComponent(dependency: dependency) + let viewController = FirstViewableRIBViewController() + let interactor = FirstViewableRIBInteractor(presenter: viewController) + interactor.listener = listener + return FirstViewableRIBRouter(interactor: interactor, viewController: viewController, secondViewableRIBBuilder: component.secondViewableRIBBuilder) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift new file mode 100644 index 0000000..7c28b97 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift @@ -0,0 +1,54 @@ +// +// FirstViewableRIBInteractor.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift + +protocol FirstViewableRIBRouting: ViewableRouting { + var firstViewableRIBViewController: FirstViewableRIBViewControllable { get } + func routeToSecondViewableRIB() + func routeAwayFromSecondViewableRIB() +} + +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? + + // TODO: Add additional dependencies to constructor. Do not perform any logic + // in constructor. + override init(presenter: FirstViewableRIBPresentable) { + 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: { [weak self] _ in + self?.router?.routeToSecondViewableRIB() + }) + .disposeOnDeactivate(interactor: self) + } + + 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..20f3ee4 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift @@ -0,0 +1,49 @@ +// +// FirstViewableRIBRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs + +protocol FirstViewableRIBInteractable: Interactable, SecondViewableRIBListener { + 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? + + init(interactor: FirstViewableRIBInteractable, viewController: FirstViewableRIBViewControllable, secondViewableRIBBuilder: SecondViewableRIBBuildable) { + self.secondViewableRIBBuilder = secondViewableRIBBuilder + 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) + } + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBViewController.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBViewController.swift new file mode 100644 index 0000000..9847ac4 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBViewController.swift @@ -0,0 +1,27 @@ +// +// FirstViewableRIBViewController.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift +import UIKit + +protocol FirstViewableRIBPresentableListener: 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 FirstViewableRIBViewController: UIViewController, FirstViewableRIBPresentable, FirstViewableRIBViewControllable { + + weak var listener: FirstViewableRIBPresentableListener? + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemGreen + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Info.plist b/Examples/RIBsAppExample2/RIBsAppExample2/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift new file mode 100644 index 0000000..29e7baf --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift @@ -0,0 +1,40 @@ +// +// 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 + +protocol RootBuildable: Buildable { + func build() -> LaunchRouting +} + +final class RootBuilder: Builder, RootBuildable { + + override init(dependency: RootDependency) { + super.init(dependency: dependency) + } + + func build() -> LaunchRouting { + let component = RootComponent(dependency: dependency) + let viewController = RootViewController() + let interactor = RootInteractor(presenter: viewController) + return RootRouter(interactor: interactor, viewController: viewController, firstViewableRIBBuilder: component.firstViewableRIBBuilder) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootInteractor.swift new file mode 100644 index 0000000..91ff2b0 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootInteractor.swift @@ -0,0 +1,47 @@ +// +// RootInteractor.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift + +protocol RootRouting: ViewableRouting { + func routeToFirstViewableRIB() + 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 { + + weak var router: RootRouting? + weak var listener: RootListener? + + // 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() + + router?.routeToFirstViewableRIB() + } + + override func willResignActive() { + super.willResignActive() + // TODO: Pause any business logic. + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootRouter.swift new file mode 100644 index 0000000..02a7bb4 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootRouter.swift @@ -0,0 +1,47 @@ +// +// RootRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs + +protocol RootInteractable: Interactable, FirstViewableRIBListener { + 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() { + let firstViewableRIBRouter = firstViewableRIBBuilder.build(withListener: interactor) + self.firstViewableRIBRouter = firstViewableRIBRouter + let firstViewableRIBViewController = firstViewableRIBRouter.firstViewableRIBViewController + viewController.embedMainView(firstViewableRIBViewController) + attachChild(firstViewableRIBRouter) + } + + 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..5e7dbff --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SceneDelegate.swift @@ -0,0 +1,31 @@ +// +// 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? + + + 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 launchRouter = RootBuilder(dependency: appComponent).build() + self.launchRouter = launchRouter + launchRouter.launch(from: window) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift new file mode 100644 index 0000000..486e885 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift @@ -0,0 +1,39 @@ +// +// 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 { + + // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. +} + +// 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 = SecondViewableRIBViewController() + let interactor = SecondViewableRIBInteractor(presenter: viewController) + interactor.listener = listener + return SecondViewableRIBRouter(interactor: interactor, viewController: viewController) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift new file mode 100644 index 0000000..9a68913 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift @@ -0,0 +1,45 @@ +// +// 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 { + // TODO: Declare methods the interactor can invoke to communicate with other RIBs. +} + +final class SecondViewableRIBInteractor: PresentableInteractor, SecondViewableRIBInteractable, SecondViewableRIBPresentableListener { + + weak var router: SecondViewableRIBRouting? + weak var listener: SecondViewableRIBListener? + + // TODO: Add additional dependencies to constructor. Do not perform any logic + // in constructor. + override init(presenter: SecondViewableRIBPresentable) { + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + // TODO: Implement business logic here. + } + + override func willResignActive() { + super.willResignActive() + // TODO: Pause any business logic. + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBRouter.swift new file mode 100644 index 0000000..3ff70f1 --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBRouter.swift @@ -0,0 +1,30 @@ +// +// SecondViewableRIBRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs + +protocol SecondViewableRIBInteractable: Interactable { + var router: SecondViewableRIBRouting? { get set } + var listener: SecondViewableRIBListener? { get set } +} + +protocol SecondViewableRIBViewControllable: ViewControllable { + +} + +final class SecondViewableRIBRouter: ViewableRouter, SecondViewableRIBRouting { + + // TODO: Constructor inject child builder protocols to allow building children. + override init(interactor: SecondViewableRIBInteractable, viewController: SecondViewableRIBViewControllable) { + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } + + var secondViewableRIBViewController: any SecondViewableRIBViewControllable { + viewController + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBViewController.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBViewController.swift new file mode 100644 index 0000000..e18803e --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBViewController.swift @@ -0,0 +1,27 @@ +// +// SecondViewableRIBViewController.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 1/10/26. +// + +import RIBs +import RxSwift +import UIKit + +protocol SecondViewableRIBPresentableListener: 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 SecondViewableRIBViewController: UIViewController, SecondViewableRIBPresentable, SecondViewableRIBViewControllable { + + weak var listener: SecondViewableRIBPresentableListener? + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .blue + } +} 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/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. + } + } + +} From 47e666c120ef647790429548481013e8755de928 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 10 Jan 2026 13:25:08 -0600 Subject: [PATCH 12/30] Make it compile with Xcode 16.4 and Xcode 26.0.1 - deinit isolation issue still persists for now, the code in them was commented out --- .../RIBsAppExample2.xcodeproj/project.pbxproj | 2 ++ RIBs/Classes/Builder.swift | 2 ++ RIBs/Classes/DI/Component.swift | 2 ++ RIBs/Classes/DI/Dependency.swift | 2 ++ RIBs/Classes/Interactor.swift | 17 +++++++---- RIBs/Classes/LaunchRouter.swift | 2 ++ RIBs/Classes/PresentableInteractor.swift | 5 +++- RIBs/Classes/Router.swift | 28 +++++++++++-------- RIBs/Classes/ViewableRouter.swift | 2 ++ RIBs/Classes/Worker/Worker.swift | 13 ++++++--- RIBs/Classes/Workflow/Workflow.swift | 3 ++ 11 files changed, 56 insertions(+), 22 deletions(-) diff --git a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj index 64bcd00..e8cc093 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj +++ b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj @@ -244,6 +244,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -272,6 +273,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; 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..73cfc83 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. @@ -140,10 +143,12 @@ open class Interactor: Interactable { fileprivate var activenessDisposable: CompositeDisposable? deinit { - if isActive { - deactivate() - } - isActiveSubject.onCompleted() + // TODO: deal with this later + +// if isActive { +// deactivate() +// } +// isActiveSubject.onCompleted() } } @@ -162,7 +167,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 +199,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/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index bafdccd..ca30f16 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`. @@ -34,6 +35,8 @@ open class PresentableInteractor: Interactor { // MARK: - Private deinit { - LeakDetector.instance.expectDeallocate(object: presenter as AnyObject) + // TODO: deal with this later + +// LeakDetector.instance.expectDeallocate(object: presenter as AnyObject) } } diff --git a/RIBs/Classes/Router.swift b/RIBs/Classes/Router.swift index de593d0..6a36f29 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`. @@ -212,16 +216,18 @@ open class Router: Routing { } deinit { - interactable.deactivate() - - if !children.isEmpty { - detachAllChildren() - } - - lifecycleSubject.onCompleted() - - deinitDisposable.dispose() - - LeakDetector.instance.expectDeallocate(object: interactable) + // TODO: get back to this + +// interactable.deactivate() +// +// if !children.isEmpty { +// detachAllChildren() +// } +// +// lifecycleSubject.onCompleted() +// +// deinitDisposable.dispose() +// +// LeakDetector.instance.expectDeallocate(object: interactable) } } diff --git a/RIBs/Classes/ViewableRouter.swift b/RIBs/Classes/ViewableRouter.swift index 82488b4..942b2c0 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`. diff --git a/RIBs/Classes/Worker/Worker.swift b/RIBs/Classes/Worker/Worker.swift index e8f4669..e05de40 100644 --- a/RIBs/Classes/Worker/Worker.swift +++ b/RIBs/Classes/Worker/Worker.swift @@ -20,6 +20,7 @@ import RxSwift /// /// `Worker`s are always bound to an `Interactor`. A `Worker` can only start if its bound `Interactor` is active. /// It is stopped when its bound interactor is deactivated. +@MainActor public protocol Working: AnyObject { /// Starts the `Worker`. @@ -46,6 +47,7 @@ public protocol Working: AnyObject { } /// The base `Worker` implementation. +@MainActor open class Worker: Working { /// Indicates if the `Worker` is started. @@ -168,9 +170,11 @@ open class Worker: Working { } deinit { - stop() - unbindInteractor() - isStartedSubject.onCompleted() + // TODO: deal with this later + +// stop() +// unbindInteractor() +// isStartedSubject.onCompleted() } } @@ -187,7 +191,7 @@ public extension Disposable { /// `worker` needs to be deallocated. /// /// - parameter worker: The `Worker` to dispose the subscription based on. - @discardableResult + @discardableResult @MainActor func disposeOnStop(_ worker: Worker) -> Disposable { if let compositeDisposable = worker.disposable { _ = compositeDisposable.insert(self) @@ -199,6 +203,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..29d8082 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() From 2d73e738d46f66563b78f7dabfbe512ad00c1d27 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 10 Jan 2026 16:53:53 -0600 Subject: [PATCH 13/30] Add MainActor conformance to the view controllable and the presenter --- .../RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj | 2 ++ RIBs/Classes/Presenter.swift | 2 ++ RIBs/Classes/ViewControllable.swift | 1 + RIBs/Classes/ViewableRouter.swift | 3 ++- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj index e8cc093..794b418 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj +++ b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj @@ -246,6 +246,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -275,6 +276,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; 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/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 942b2c0..423ea1b 100644 --- a/RIBs/Classes/ViewableRouter.swift +++ b/RIBs/Classes/ViewableRouter.swift @@ -92,6 +92,7 @@ open class ViewableRouter: Router Date: Sat, 10 Jan 2026 17:35:39 -0600 Subject: [PATCH 14/30] make deinits isolated for swift strict concurrency --- RIBs/Classes/Interactor.swift | 12 +++++------ RIBs/Classes/PresentableInteractor.swift | 6 ++---- RIBs/Classes/Router.swift | 27 ++++++++++++------------ RIBs/Classes/ViewableRouter.swift | 5 ++--- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/RIBs/Classes/Interactor.swift b/RIBs/Classes/Interactor.swift index 73cfc83..d123c79 100644 --- a/RIBs/Classes/Interactor.swift +++ b/RIBs/Classes/Interactor.swift @@ -142,13 +142,11 @@ open class Interactor: Interactable { private let isActiveSubject = BehaviorSubject(value: false) fileprivate var activenessDisposable: CompositeDisposable? - deinit { - // TODO: deal with this later - -// if isActive { -// deactivate() -// } -// isActiveSubject.onCompleted() + isolated deinit { + if isActive { + deactivate() + } + isActiveSubject.onCompleted() } } diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index ca30f16..38e9b98 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -34,9 +34,7 @@ open class PresentableInteractor: Interactor { // MARK: - Private - deinit { - // TODO: deal with this later - -// LeakDetector.instance.expectDeallocate(object: presenter as AnyObject) + isolated deinit { + LeakDetector.instance.expectDeallocate(object: presenter as AnyObject) } } diff --git a/RIBs/Classes/Router.swift b/RIBs/Classes/Router.swift index 6a36f29..1371702 100644 --- a/RIBs/Classes/Router.swift +++ b/RIBs/Classes/Router.swift @@ -214,20 +214,19 @@ open class Router: Routing { detachChild(child) } } - - deinit { - // TODO: get back to this + + isolated deinit { + interactable.deactivate() + + if !children.isEmpty { + detachAllChildren() + } + + lifecycleSubject.onCompleted() + + deinitDisposable.dispose() + + LeakDetector.instance.expectDeallocate(object: interactable) -// interactable.deactivate() -// -// if !children.isEmpty { -// detachAllChildren() -// } -// -// lifecycleSubject.onCompleted() -// -// deinitDisposable.dispose() -// -// LeakDetector.instance.expectDeallocate(object: interactable) } } diff --git a/RIBs/Classes/ViewableRouter.swift b/RIBs/Classes/ViewableRouter.swift index 423ea1b..e562cc3 100644 --- a/RIBs/Classes/ViewableRouter.swift +++ b/RIBs/Classes/ViewableRouter.swift @@ -91,8 +91,7 @@ open class ViewableRouter: Router Date: Sat, 10 Jan 2026 17:40:36 -0600 Subject: [PATCH 15/30] Make worker's deinit be isolated --- RIBs/Classes/Worker/Worker.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/RIBs/Classes/Worker/Worker.swift b/RIBs/Classes/Worker/Worker.swift index e05de40..ccd0963 100644 --- a/RIBs/Classes/Worker/Worker.swift +++ b/RIBs/Classes/Worker/Worker.swift @@ -169,12 +169,10 @@ open class Worker: Working { interactorBindingDisposable = nil } - deinit { - // TODO: deal with this later - -// stop() -// unbindInteractor() -// isStartedSubject.onCompleted() + isolated deinit { + stop() + unbindInteractor() + isStartedSubject.onCompleted() } } From 16a796ea9bea90def145c091b351d1ae46af52a1 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 10 Jan 2026 18:27:59 -0600 Subject: [PATCH 16/30] Mark leak detector and workflow helper methods as main actor --- .../RIBsAppExample2.xcodeproj/project.pbxproj | 6 ++++-- RIBs/Classes/LeakDetector/LeakDetector.swift | 1 + RIBs/Classes/Workflow/Workflow.swift | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj index 794b418..9b05419 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj +++ b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj @@ -242,11 +242,12 @@ "@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_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -272,11 +273,12 @@ "@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_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; 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/Workflow/Workflow.swift b/RIBs/Classes/Workflow/Workflow.swift index 29d8082..40f1e2a 100644 --- a/RIBs/Classes/Workflow/Workflow.swift +++ b/RIBs/Classes/Workflow/Workflow.swift @@ -209,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) } @@ -223,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) } From f8f443d7368c97a8efce5689f3182577aa928fdb Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Mon, 16 Feb 2026 22:35:33 -0600 Subject: [PATCH 17/30] Add Rx service example, add actor service example, add worker example --- .../RIBsAppExample2.xcodeproj/project.pbxproj | 26 ++++++--- .../FirstViewableRIB/ActorService.swift | 25 +++++++++ .../FirstViewableRIBBuilder.swift | 10 +++- .../FirstViewableRIBInteractor.swift | 53 +++++++++++++++++-- .../FirstViewableRIB/RxSwiftService.swift | 36 +++++++++++++ .../SecondViewableRIB/ExampleWorker.swift | 28 ++++++++++ .../SecondViewableRIBBuilder.swift | 3 +- .../SecondViewableRIBInteractor.swift | 15 ++++-- .../SecondViewableRIBViewController.swift | 23 ++++++-- 9 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/ActorService.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/RxSwiftService.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/ExampleWorker.swift diff --git a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj index 9b05419..fec87f3 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj +++ b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj @@ -7,7 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 7E71E73E2F4426FA002FD889 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7E71E73D2F4426FA002FD889 /* 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 */ @@ -56,7 +58,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7EFF41982F17D99D00B48704 /* RIBs in Frameworks */, 7EB78D4F2F12CC0000547345 /* RIBs in Frameworks */, + 7E71E73E2F4426FA002FD889 /* RIBs in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -109,6 +113,8 @@ name = RIBsAppExample2; packageProductDependencies = ( 7EB78D4E2F12CC0000547345 /* RIBs */, + 7EFF41972F17D99D00B48704 /* RIBs */, + 7E71E73D2F4426FA002FD889 /* RIBs */, ); productName = RIBsAppExample2; productReference = 7EB78D192F12CB3800547345 /* RIBsAppExample2.app */; @@ -166,7 +172,7 @@ mainGroup = 7EB78D102F12CB3800547345; minimizedProjectReferenceProxies = 1; packageReferences = ( - 7EB78D4D2F12CC0000547345 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */, + 7E71E73C2F4426FA002FD889 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */, ); preferredProjectObjectVersion = 77; productRefGroup = 7EB78D1A2F12CB3800547345 /* Products */; @@ -247,8 +253,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = minimal; - SWIFT_VERSION = 5.0; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -278,8 +284,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = minimal; - SWIFT_VERSION = 5.0; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -476,17 +482,25 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 7EB78D4D2F12CC0000547345 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */ = { + 7E71E73C2F4426FA002FD889 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */ = { isa = XCLocalSwiftPackageReference; relativePath = "../../../RIBs-iOS"; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 7E71E73D2F4426FA002FD889 /* 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/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/FirstViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift index 72f5702..ef7e5c6 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift @@ -14,6 +14,14 @@ protocol FirstViewableRIBDependency: Dependency { final class FirstViewableRIBComponent: Component, SecondViewableRIBDependency { + var actorService: ActorServicable { + ActorService() + } + + var rxSwiftService: RxSwiftServicable { + RxSwiftService() + } + var secondViewableRIBBuilder: SecondViewableRIBBuildable { SecondViewableRIBBuilder(dependency: self) } @@ -34,7 +42,7 @@ final class FirstViewableRIBBuilder: Builder, FirstV func build(withListener listener: FirstViewableRIBListener) -> FirstViewableRIBRouting { let component = FirstViewableRIBComponent(dependency: dependency) let viewController = FirstViewableRIBViewController() - let interactor = FirstViewableRIBInteractor(presenter: viewController) + let interactor = FirstViewableRIBInteractor(presenter: viewController, actorService: component.actorService, rxSwiftService: component.rxSwiftService) interactor.listener = listener return FirstViewableRIBRouter(interactor: interactor, viewController: viewController, secondViewableRIBBuilder: component.secondViewableRIBBuilder) } diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift index 7c28b97..f021a7c 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift @@ -7,6 +7,7 @@ import RIBs import RxSwift +import Foundation protocol FirstViewableRIBRouting: ViewableRouting { var firstViewableRIBViewController: FirstViewableRIBViewControllable { get } @@ -27,10 +28,13 @@ final class FirstViewableRIBInteractor: PresentableInteractor .timer(.seconds(3), scheduler: ConcurrentDispatchQueueScheduler(qos: .userInitiated)) .observe(on: MainScheduler.instance) - .subscribe(onSuccess: { [weak self] _ in - self?.router?.routeToSecondViewableRIB() + .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() } override func willResignActive() { 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/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 index 486e885..afd683a 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift @@ -32,7 +32,8 @@ final class SecondViewableRIBBuilder: Builder, Seco func build(withListener listener: SecondViewableRIBListener) -> SecondViewableRIBRouting { let component = SecondViewableRIBComponent(dependency: dependency) let viewController = SecondViewableRIBViewController() - let interactor = SecondViewableRIBInteractor(presenter: viewController) + let exampleWorker = ExampleWorkerImp() + let interactor = SecondViewableRIBInteractor(presenter: viewController, exampleWorker: exampleWorker) interactor.listener = listener return SecondViewableRIBRouter(interactor: interactor, viewController: viewController) } diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift index 9a68913..bd82d85 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift @@ -18,7 +18,7 @@ protocol SecondViewableRIBPresentable: Presentable { } protocol SecondViewableRIBListener: AnyObject { - // TODO: Declare methods the interactor can invoke to communicate with other RIBs. + func didComplete(_ secondViewableRIB: SecondViewableRIBInteractable) } final class SecondViewableRIBInteractor: PresentableInteractor, SecondViewableRIBInteractable, SecondViewableRIBPresentableListener { @@ -26,16 +26,21 @@ final class SecondViewableRIBInteractor: PresentableInteractor Date: Tue, 10 Mar 2026 14:37:48 -0500 Subject: [PATCH 18/30] Fix tests --- .github/workflows/iOS.yml | 4 ++-- RIBsTests/ComponentizedBuilderTests.swift | 1 + RIBsTests/DI/ComponentTests.swift | 1 + RIBsTests/Interactor/InteractorTests.swift | 6 +++--- RIBsTests/Interactor/PresentableInteractorTests.swift | 3 ++- RIBsTests/LaunchRouterTests.swift | 1 + RIBsTests/MultiStageComponentizedBuilderTests.swift | 1 + RIBsTests/Router/RouterTests.swift | 7 ++++--- RIBsTests/Router/ViewableRouterTests.swift | 3 ++- RIBsTests/Worker/WorkerTests.swift | 1 + RIBsTests/Workflow/WorkflowTests.swift | 1 + 11 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/workflows/iOS.yml b/.github/workflows/iOS.yml index d0929ad..f90091d 100644 --- a/.github/workflows/iOS.yml +++ b/.github/workflows/iOS.yml @@ -19,11 +19,11 @@ 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 \ 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() { From 3d69a300ab98dcd2a54aa2823aee16f0325181e5 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Tue, 10 Mar 2026 14:39:52 -0500 Subject: [PATCH 19/30] Fix CI --- Examples/Example1/Podfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example1/Podfile b/Examples/Example1/Podfile index 6c660d9..d3fed94 100644 --- a/Examples/Example1/Podfile +++ b/Examples/Example1/Podfile @@ -3,6 +3,6 @@ use_frameworks! platform :ios, '15.0' target 'RIBs_Example' do - pod 'RIBs', :path => '../', :testspecs => ['Tests'] + pod 'RIBs', :path => '../../', :testspecs => ['Tests'] end From 104051051c0d8ac547154ef6793d17dea695c725 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Tue, 10 Mar 2026 14:53:44 -0500 Subject: [PATCH 20/30] Fix CI --- .github/workflows/iOS.yml | 2 +- Examples/Example1/Podfile | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/iOS.yml b/.github/workflows/iOS.yml index f90091d..9a75c00 100644 --- a/.github/workflows/iOS.yml +++ b/.github/workflows/iOS.yml @@ -29,7 +29,7 @@ jobs: -workspace RIBs.xcworkspace \ -scheme RIBs-Example \ -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ + -destination 'generic/platform=iOS Simulator' \ -enableCodeCoverage YES \ clean test diff --git a/Examples/Example1/Podfile b/Examples/Example1/Podfile index d3fed94..07090ee 100644 --- a/Examples/Example1/Podfile +++ b/Examples/Example1/Podfile @@ -6,3 +6,11 @@ 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 From 22ac1b7beeb45e0802ff67b4a025ca1c9b5f14e4 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Tue, 10 Mar 2026 14:57:20 -0500 Subject: [PATCH 21/30] Fix CI --- .github/workflows/iOS.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/iOS.yml b/.github/workflows/iOS.yml index 9a75c00..9faeeb0 100644 --- a/.github/workflows/iOS.yml +++ b/.github/workflows/iOS.yml @@ -29,7 +29,7 @@ jobs: -workspace RIBs.xcworkspace \ -scheme RIBs-Example \ -sdk iphonesimulator \ - -destination 'generic/platform=iOS Simulator' \ + -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation),OS=18.0' \ -enableCodeCoverage YES \ clean test From eae2d1555b0cb6d1ee20771ee540dcf45657e021 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Tue, 10 Mar 2026 14:59:52 -0500 Subject: [PATCH 22/30] Fix CI --- .github/workflows/iOS.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/iOS.yml b/.github/workflows/iOS.yml index 9faeeb0..d4f10b5 100644 --- a/.github/workflows/iOS.yml +++ b/.github/workflows/iOS.yml @@ -29,7 +29,7 @@ jobs: -workspace RIBs.xcworkspace \ -scheme RIBs-Example \ -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation),OS=18.0' \ + -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation),OS=18.5' \ -enableCodeCoverage YES \ clean test From a23806364a015b5ce66a0e63917c3a09419b4f37 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Tue, 10 Mar 2026 15:04:51 -0500 Subject: [PATCH 23/30] Fix CI --- RIBs.podspec | 1 + 1 file changed, 1 insertion(+) diff --git a/RIBs.podspec b/RIBs.podspec index 3f41d28..a25bdb1 100644 --- a/RIBs.podspec +++ b/RIBs.podspec @@ -11,6 +11,7 @@ RIBs is the cross-platform architecture behind many mobile apps at Uber. This ar s.source = { :git => 'https://github.com/uber/RIBs-iOS.git', :tag => s.version.to_s } s.ios.deployment_target = '15.0' s.swift_version = '5.0' + s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '$(inherited) -enable-experimental-feature IsolatedDeinit' } s.source_files = 'RIBs/Classes/**/*' s.dependency 'RxSwift', '~> 6.9.0' s.dependency 'RxRelay', '~> 6.9.0' From d745e5ee990c68f7e8b5bb1f7044c56f4ddc79a2 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Tue, 10 Mar 2026 15:10:31 -0500 Subject: [PATCH 24/30] Fix CI --- .github/workflows/iOS.yml | 5 ++++- RIBs.podspec | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/iOS.yml b/.github/workflows/iOS.yml index d4f10b5..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: diff --git a/RIBs.podspec b/RIBs.podspec index a25bdb1..3f41d28 100644 --- a/RIBs.podspec +++ b/RIBs.podspec @@ -11,7 +11,6 @@ RIBs is the cross-platform architecture behind many mobile apps at Uber. This ar s.source = { :git => 'https://github.com/uber/RIBs-iOS.git', :tag => s.version.to_s } s.ios.deployment_target = '15.0' s.swift_version = '5.0' - s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '$(inherited) -enable-experimental-feature IsolatedDeinit' } s.source_files = 'RIBs/Classes/**/*' s.dependency 'RxSwift', '~> 6.9.0' s.dependency 'RxRelay', '~> 6.9.0' From 7d1f1de6b5b0e3f8f277fac9c68427b2f7aa620d Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Tue, 10 Mar 2026 15:42:15 -0500 Subject: [PATCH 25/30] Add headless RIB example --- .../SecondViewableRIBBuilder.swift | 18 +++++-- .../SecondViewableRIBInteractor.swift | 7 ++- .../SecondViewableRIBRouter.swift | 23 +++++++-- .../ThirdHeadlessRIBBuilder.swift | 44 ++++++++++++++++ .../ThirdHeadlessRIBInteractor.swift | 51 +++++++++++++++++++ .../ThirdHeadlessRIBRouter.swift | 38 ++++++++++++++ 6 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBBuilder.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBInteractor.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift index afd683a..9501c9c 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift @@ -12,9 +12,19 @@ protocol SecondViewableRIBDependency: Dependency { // created by this RIB. } -final class SecondViewableRIBComponent: Component { +final class SecondViewableRIBComponent: Component, ThirdHeadlessRIBDependency { - // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. + var thirdHeadlessRIBBuilder: ThirdHeadlessRIBBuildable { + ThirdHeadlessRIBBuilder(dependency: self) + } + + var viewController: SecondViewableRIBPresentable & SecondViewableRIBViewControllable { + SecondViewableRIBViewController() + } + + var thirdHeadlessRIBViewController: any ThirdHeadlessRIBViewControllable { + viewController + } } // MARK: - Builder @@ -31,10 +41,10 @@ final class SecondViewableRIBBuilder: Builder, Seco func build(withListener listener: SecondViewableRIBListener) -> SecondViewableRIBRouting { let component = SecondViewableRIBComponent(dependency: dependency) - let viewController = SecondViewableRIBViewController() + let viewController = component.viewController let exampleWorker = ExampleWorkerImp() let interactor = SecondViewableRIBInteractor(presenter: viewController, exampleWorker: exampleWorker) interactor.listener = listener - return SecondViewableRIBRouter(interactor: interactor, viewController: viewController) + 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 index bd82d85..eb00755 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBInteractor.swift @@ -45,6 +45,11 @@ final class SecondViewableRIBInteractor: PresentableInteractor, SecondViewableRIBRouting { + + private let thirdHeadlessRIBBuilder: ThirdHeadlessRIBBuildable + private var thirdHeadlessRIBRouter: ThirdHeadlessRIBRouting? - // TODO: Constructor inject child builder protocols to allow building children. - override init(interactor: SecondViewableRIBInteractable, viewController: SecondViewableRIBViewControllable) { + init(interactor: SecondViewableRIBInteractable, viewController: SecondViewableRIBViewControllable, thirdHeadlessRIBBuilder: ThirdHeadlessRIBBuildable) { + self.thirdHeadlessRIBBuilder = thirdHeadlessRIBBuilder super.init(interactor: interactor, viewController: viewController) interactor.router = self } @@ -27,4 +30,16 @@ final class SecondViewableRIBRouter: ViewableRouter { + + fileprivate var thirdHeadlessRIBViewController: ThirdHeadlessRIBViewControllable { + return dependency.thirdHeadlessRIBViewController + } + + // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. +} + +// 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) + } +} diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBInteractor.swift new file mode 100644 index 0000000..deeaffb --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBInteractor.swift @@ -0,0 +1,51 @@ +// +// 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. +} + +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) + }) + .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..f13f5af --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift @@ -0,0 +1,38 @@ +// +// ThirdHeadlessRIBRouter.swift +// RIBsAppExample2 +// +// Created by Alex Bush on 3/10/26. +// + +import RIBs + +protocol ThirdHeadlessRIBInteractable: Interactable { + 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 { + + // TODO: Constructor inject child builder protocols to allow building children. + init(interactor: ThirdHeadlessRIBInteractable, viewController: ThirdHeadlessRIBViewControllable) { + self.viewController = viewController + 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. + } + + // MARK: - Private + + private let viewController: ThirdHeadlessRIBViewControllable +} From 6f0bd36154b6df1afd0ca47b0b393aad5bd16613 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Wed, 11 Mar 2026 13:38:17 -0500 Subject: [PATCH 26/30] Add presenter example --- .../FourthViewableRIBBuilder.swift | 40 ++++++++++++++ .../FourthViewableRIBInteractor.swift | 55 +++++++++++++++++++ .../FourthViewableRIBPresenter.swift | 23 ++++++++ .../FourthViewableRIBRouter.swift | 26 +++++++++ .../FourthViewableRIBViewController.swift | 29 ++++++++++ .../SecondViewableRIBBuilder.swift | 2 +- .../ThirdHeadlessRIBBuilder.swift | 8 ++- .../ThirdHeadlessRIBInteractor.swift | 3 + .../ThirdHeadlessRIBRouter.swift | 30 ++++++++-- 9 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBInteractor.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBPresenter.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBRouter.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBViewController.swift diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift new file mode 100644 index 0000000..41f39aa --- /dev/null +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift @@ -0,0 +1,40 @@ +// +// 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) -> FourthViewableRIBRouting +} + +final class FourthViewableRIBBuilder: Builder, FourthViewableRIBBuildable { + + override init(dependency: FourthViewableRIBDependency) { + super.init(dependency: dependency) + } + + func build(withListener listener: FourthViewableRIBListener) -> FourthViewableRIBRouting { + let component = FourthViewableRIBComponent(dependency: dependency) + let viewController = FourthViewableRIBViewController() + let presenter = FourthViewableRIBPresenter(viewController: viewController) + let interactor = FourthViewableRIBInteractor(presenter: presenter) + interactor.listener = listener + return FourthViewableRIBRouter(interactor: interactor, viewController: viewController) + } +} 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..28e4154 --- /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 { + 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/SecondViewableRIB/SecondViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift index 9501c9c..c2eea8e 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SecondViewableRIB/SecondViewableRIBBuilder.swift @@ -19,7 +19,7 @@ final class SecondViewableRIBComponent: Component, } var viewController: SecondViewableRIBPresentable & SecondViewableRIBViewControllable { - SecondViewableRIBViewController() + shared { SecondViewableRIBViewController() } } var thirdHeadlessRIBViewController: any ThirdHeadlessRIBViewControllable { diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBBuilder.swift index be613db..02049f9 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBBuilder.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBBuilder.swift @@ -14,13 +14,15 @@ protocol ThirdHeadlessRIBDependency: Dependency { // created by this RIB. } -final class ThirdHeadlessRIBComponent: Component { +final class ThirdHeadlessRIBComponent: Component, FourthViewableRIBDependency { fileprivate var thirdHeadlessRIBViewController: ThirdHeadlessRIBViewControllable { return dependency.thirdHeadlessRIBViewController } - // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. + fileprivate var fourthViewableRIBBuilder: FourthViewableRIBBuildable { + FourthViewableRIBBuilder(dependency: self) + } } // MARK: - Builder @@ -39,6 +41,6 @@ final class ThirdHeadlessRIBBuilder: Builder, ThirdH let component = ThirdHeadlessRIBComponent(dependency: dependency) let interactor = ThirdHeadlessRIBInteractor() interactor.listener = listener - return ThirdHeadlessRIBRouter(interactor: interactor, viewController: component.thirdHeadlessRIBViewController) + 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 index deeaffb..4c00751 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBInteractor.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBInteractor.swift @@ -11,6 +11,8 @@ 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 { @@ -38,6 +40,7 @@ final class ThirdHeadlessRIBInteractor: Interactor, ThirdHeadlessRIBInteractable .observe(on: MainScheduler.instance) .subscribe(onSuccess: { _ in self.listener?.sendData(self) + self.router?.routeToFourthRIB() }) .disposeOnDeactivate(interactor: self) } diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift index f13f5af..eed824a 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift @@ -7,7 +7,7 @@ import RIBs -protocol ThirdHeadlessRIBInteractable: Interactable { +protocol ThirdHeadlessRIBInteractable: Interactable, FourthViewableRIBListener { var router: ThirdHeadlessRIBRouting? { get set } var listener: ThirdHeadlessRIBListener? { get set } } @@ -19,10 +19,16 @@ protocol ThirdHeadlessRIBViewControllable: ViewControllable { } 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) { + init(interactor: ThirdHeadlessRIBInteractable, viewController: ThirdHeadlessRIBViewControllable, fourthViewableRIBBuilder: FourthViewableRIBBuildable) { self.viewController = viewController + self.fourthViewableRIBBuilder = fourthViewableRIBBuilder super.init(interactor: interactor) interactor.router = self } @@ -30,9 +36,23 @@ final class ThirdHeadlessRIBRouter: Router, ThirdH 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() } - // MARK: - Private - - private let viewController: ThirdHeadlessRIBViewControllable + + func routeToFourthRIB() { + let fourthViewableRIBRouter = 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) + } + } } From 580da97fd857e2346c5c7a2a9075861691fedc39 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Wed, 11 Mar 2026 17:32:30 -0500 Subject: [PATCH 27/30] Add workflow example --- .../RIBsAppExample2.xcodeproj/project.pbxproj | 36 +++++++++----- .../FirstViewableRIBBuilder.swift | 19 +++++--- .../FirstViewableRIBInteractor.swift | 12 +++++ .../FirstViewableRIBRouter.swift | 24 +++++++++- .../FourthViewableRIBBuilder.swift | 7 +-- .../FourthViewableRIBRouter.swift | 2 +- .../RIBsAppExample2/Info.plist | 11 +++++ .../RIBsAppExample2/Root/RootBuilder.swift | 14 ++++-- .../RIBsAppExample2/Root/RootInteractor.swift | 31 ++++++++++-- .../RIBsAppExample2/Root/RootRouter.swift | 13 ++--- .../RIBsAppExample2/SceneDelegate.swift | 24 ++++++---- .../ThirdHeadlessRIBRouter.swift | 2 +- .../OpenFourthViewableRIBWorkflow.swift | 47 +++++++++++++++++++ .../Workflows/UrlHandler.swift | 12 +++++ 14 files changed, 205 insertions(+), 49 deletions(-) create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Workflows/OpenFourthViewableRIBWorkflow.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/Workflows/UrlHandler.swift diff --git a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj index fec87f3..b62adf5 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj +++ b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 7E71E73E2F4426FA002FD889 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7E71E73D2F4426FA002FD889 /* RIBs */; }; + 7EA3A6CE2F61EE4A00D01810 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EA3A6CD2F61EE4A00D01810 /* RIBs */; }; 7EB78D4F2F12CC0000547345 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EB78D4E2F12CC0000547345 /* RIBs */; }; 7EFF41982F17D99D00B48704 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EFF41972F17D99D00B48704 /* RIBs */; }; /* End PBXBuildFile section */ @@ -58,6 +59,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7EA3A6CE2F61EE4A00D01810 /* RIBs in Frameworks */, 7EFF41982F17D99D00B48704 /* RIBs in Frameworks */, 7EB78D4F2F12CC0000547345 /* RIBs in Frameworks */, 7E71E73E2F4426FA002FD889 /* RIBs in Frameworks */, @@ -115,6 +117,7 @@ 7EB78D4E2F12CC0000547345 /* RIBs */, 7EFF41972F17D99D00B48704 /* RIBs */, 7E71E73D2F4426FA002FD889 /* RIBs */, + 7EA3A6CD2F61EE4A00D01810 /* RIBs */, ); productName = RIBsAppExample2; productReference = 7EB78D192F12CB3800547345 /* RIBsAppExample2.app */; @@ -172,7 +175,7 @@ mainGroup = 7EB78D102F12CB3800547345; minimizedProjectReferenceProxies = 1; packageReferences = ( - 7E71E73C2F4426FA002FD889 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */, + 7EA3A6CC2F61EE4A00D01810 /* XCRemoteSwiftPackageReference "RIBs-iOS" */, ); preferredProjectObjectVersion = 77; productRefGroup = 7EB78D1A2F12CB3800547345 /* Products */; @@ -251,10 +254,10 @@ OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_STRICT_CONCURRENCY = minimal; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -282,10 +285,10 @@ OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_STRICT_CONCURRENCY = minimal; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -481,18 +484,27 @@ }; /* End XCConfigurationList section */ -/* Begin XCLocalSwiftPackageReference section */ - 7E71E73C2F4426FA002FD889 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = "../../../RIBs-iOS"; +/* Begin XCRemoteSwiftPackageReference section */ + 7EA3A6CC2F61EE4A00D01810 /* XCRemoteSwiftPackageReference "RIBs-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/uber/RIBs-iOS.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; }; -/* End XCLocalSwiftPackageReference section */ +/* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 7E71E73D2F4426FA002FD889 /* RIBs */ = { isa = XCSwiftPackageProductDependency; productName = RIBs; }; + 7EA3A6CD2F61EE4A00D01810 /* RIBs */ = { + isa = XCSwiftPackageProductDependency; + package = 7EA3A6CC2F61EE4A00D01810 /* XCRemoteSwiftPackageReference "RIBs-iOS" */; + productName = RIBs; + }; 7EB78D4E2F12CC0000547345 /* RIBs */ = { isa = XCSwiftPackageProductDependency; productName = RIBs; diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift index ef7e5c6..b265a28 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift @@ -12,25 +12,29 @@ protocol FirstViewableRIBDependency: Dependency { // created by this RIB. } -final class FirstViewableRIBComponent: Component, SecondViewableRIBDependency { - +final class FirstViewableRIBComponent: Component, SecondViewableRIBDependency, FourthViewableRIBDependency { + var actorService: ActorServicable { ActorService() } - + var rxSwiftService: RxSwiftServicable { RxSwiftService() } - + var secondViewableRIBBuilder: SecondViewableRIBBuildable { SecondViewableRIBBuilder(dependency: self) } + + var fourthViewableRIBBuilder: FourthViewableRIBBuildable { + FourthViewableRIBBuilder(dependency: self) + } } // MARK: - Builder protocol FirstViewableRIBBuildable: Buildable { - func build(withListener listener: FirstViewableRIBListener) -> FirstViewableRIBRouting + func build(withListener listener: FirstViewableRIBListener) -> (routing: FirstViewableRIBRouting, actionableItem: FirstViewableRIBActionableItem) } final class FirstViewableRIBBuilder: Builder, FirstViewableRIBBuildable { @@ -39,11 +43,12 @@ final class FirstViewableRIBBuilder: Builder, FirstV super.init(dependency: dependency) } - func build(withListener listener: FirstViewableRIBListener) -> FirstViewableRIBRouting { + 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) interactor.listener = listener - return FirstViewableRIBRouter(interactor: interactor, viewController: viewController, secondViewableRIBBuilder: component.secondViewableRIBBuilder) + let router = FirstViewableRIBRouter(interactor: interactor, viewController: viewController, secondViewableRIBBuilder: component.secondViewableRIBBuilder, fourthViewableRIBBuilder: component.fourthViewableRIBBuilder) + return (routing: router, actionableItem: interactor) } } diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift index f021a7c..efa1bb4 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift @@ -9,10 +9,13 @@ import RIBs import RxSwift import Foundation + protocol FirstViewableRIBRouting: ViewableRouting { var firstViewableRIBViewController: FirstViewableRIBViewControllable { get } func routeToSecondViewableRIB() func routeAwayFromSecondViewableRIB() + func routeToFourthViewableRIB() -> FourthViewableRIBActionableItem + func routeAwayFromFourthViewableRIB() } protocol FirstViewableRIBPresentable: Presentable { @@ -90,6 +93,15 @@ final class FirstViewableRIBInteractor: PresentableInteractor 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 index 20f3ee4..d9b0fa1 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift @@ -7,7 +7,7 @@ import RIBs -protocol FirstViewableRIBInteractable: Interactable, SecondViewableRIBListener { +protocol FirstViewableRIBInteractable: Interactable, FirstViewableRIBActionableItem, SecondViewableRIBListener, FourthViewableRIBListener { var router: FirstViewableRIBRouting? { get set } var listener: FirstViewableRIBListener? { get set } } @@ -21,8 +21,12 @@ final class FirstViewableRIBRouter: ViewableRouter 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) + } + } } diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift index 41f39aa..6cfb884 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBBuilder.swift @@ -20,7 +20,7 @@ final class FourthViewableRIBComponent: Component { // MARK: - Builder protocol FourthViewableRIBBuildable: Buildable { - func build(withListener listener: FourthViewableRIBListener) -> FourthViewableRIBRouting + func build(withListener listener: FourthViewableRIBListener) -> (routing: FourthViewableRIBRouting, actionableItem: FourthViewableRIBActionableItem) } final class FourthViewableRIBBuilder: Builder, FourthViewableRIBBuildable { @@ -29,12 +29,13 @@ final class FourthViewableRIBBuilder: Builder, Four super.init(dependency: dependency) } - func build(withListener listener: FourthViewableRIBListener) -> FourthViewableRIBRouting { + 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 - return FourthViewableRIBRouter(interactor: interactor, viewController: viewController) + let router = FourthViewableRIBRouter(interactor: interactor, viewController: viewController) + return (routing: router, actionableItem: interactor) } } diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBRouter.swift index 28e4154..4bb760b 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBRouter.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FourthViewableRIB/FourthViewableRIBRouter.swift @@ -7,7 +7,7 @@ import RIBs -protocol FourthViewableRIBInteractable: Interactable { +protocol FourthViewableRIBInteractable: Interactable, FourthViewableRIBActionableItem { var router: FourthViewableRIBRouting? { get set } var listener: FourthViewableRIBListener? { get set } } diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Info.plist b/Examples/RIBsAppExample2/RIBsAppExample2/Info.plist index dd3c9af..e0c227b 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/Info.plist +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Info.plist @@ -2,6 +2,17 @@ + CFBundleURLTypes + + + CFBundleURLSchemes + + ribsappexample2 + + CFBundleURLName + RIBsAppExample2 Deep Link + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift index 29e7baf..fb1b7ed 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootBuilder.swift @@ -12,7 +12,7 @@ protocol RootDependency: Dependency { // created by this RIB. } -final class RootComponent: Component, FirstViewableRIBDependency { +final class RootComponent: Component, FirstViewableRIBDependency { var firstViewableRIBBuilder: FirstViewableRIBBuildable { FirstViewableRIBBuilder(dependency: self) @@ -21,8 +21,13 @@ final class RootComponent: Component, FirstViewableRIBDependency // MARK: - Builder +struct RootBuildResult { + let launchRouter: LaunchRouting + let urlHandler: UrlHandler +} + protocol RootBuildable: Buildable { - func build() -> LaunchRouting + func build() -> RootBuildResult } final class RootBuilder: Builder, RootBuildable { @@ -31,10 +36,11 @@ final class RootBuilder: Builder, RootBuildable { super.init(dependency: dependency) } - func build() -> LaunchRouting { + func build() -> RootBuildResult { let component = RootComponent(dependency: dependency) let viewController = RootViewController() let interactor = RootInteractor(presenter: viewController) - return RootRouter(interactor: interactor, viewController: viewController, firstViewableRIBBuilder: component.firstViewableRIBBuilder) + 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 index 91ff2b0..609e579 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootInteractor.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootInteractor.swift @@ -7,9 +7,10 @@ import RIBs import RxSwift +import Foundation protocol RootRouting: ViewableRouting { - func routeToFirstViewableRIB() + func routeToFirstViewableRIB() -> FirstViewableRIBActionableItem func routeAwayFromFirstViewableRIB() } @@ -22,11 +23,13 @@ protocol RootListener: AnyObject { // TODO: Declare methods the interactor can invoke to communicate with other RIBs. } -final class RootInteractor: PresentableInteractor, RootInteractable, RootPresentableListener { +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) { @@ -36,12 +39,32 @@ final class RootInteractor: PresentableInteractor, RootInteract override func didBecomeActive() { super.didBecomeActive() - - router?.routeToFirstViewableRIB() + + 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 index 02a7bb4..b581397 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootRouter.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/Root/RootRouter.swift @@ -7,7 +7,7 @@ import RIBs -protocol RootInteractable: Interactable, FirstViewableRIBListener { +protocol RootInteractable: Interactable, FirstViewableRIBListener, RootActionableItem { var router: RootRouting? { get set } var listener: RootListener? { get set } } @@ -18,7 +18,7 @@ protocol RootViewControllable: ViewControllable { } final class RootRouter: LaunchRouter, RootRouting { - + private let firstViewableRIBBuilder: FirstViewableRIBBuildable private var firstViewableRIBRouter: FirstViewableRIBRouting? @@ -27,15 +27,16 @@ final class RootRouter: LaunchRouter, Ro super.init(interactor: interactor, viewController: viewController) interactor.router = self } - - func routeToFirstViewableRIB() { - let firstViewableRIBRouter = firstViewableRIBBuilder.build(withListener: interactor) + + 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 diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/SceneDelegate.swift b/Examples/RIBsAppExample2/RIBsAppExample2/SceneDelegate.swift index 5e7dbff..75d5620 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/SceneDelegate.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/SceneDelegate.swift @@ -11,21 +11,27 @@ import RIBs class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - - private var launchRouter: LaunchRouting? + 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 launchRouter = RootBuilder(dependency: appComponent).build() - self.launchRouter = launchRouter - launchRouter.launch(from: 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/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift index eed824a..6ca3acb 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/ThirdHeadlessRIB/ThirdHeadlessRIBRouter.swift @@ -41,7 +41,7 @@ final class ThirdHeadlessRIBRouter: Router, ThirdH func routeToFourthRIB() { - let fourthViewableRIBRouter = fourthViewableRIBBuilder.build(withListener: interactor) + let (fourthViewableRIBRouter, fourthViewableRIBInteractor) = fourthViewableRIBBuilder.build(withListener: interactor) self.fourthViewableRIBRouter = fourthViewableRIBRouter let fourthViewableRIBViewControllable = fourthViewableRIBRouter.viewControllable attachChild(fourthViewableRIBRouter) 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) +} From 4ea89b9b43a5e8ca0b511b21828cc225adb628c9 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Wed, 11 Mar 2026 17:36:15 -0500 Subject: [PATCH 28/30] Restore project settings to defaults for brand new projects in Xcode 26 --- .../RIBsAppExample2.xcodeproj/project.pbxproj | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj index b62adf5..bff5499 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj +++ b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 7E71E73E2F4426FA002FD889 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7E71E73D2F4426FA002FD889 /* RIBs */; }; - 7EA3A6CE2F61EE4A00D01810 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EA3A6CD2F61EE4A00D01810 /* RIBs */; }; + 7EA3A6D62F62251D00D01810 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EA3A6D52F62251D00D01810 /* RIBs */; }; 7EB78D4F2F12CC0000547345 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EB78D4E2F12CC0000547345 /* RIBs */; }; 7EFF41982F17D99D00B48704 /* RIBs in Frameworks */ = {isa = PBXBuildFile; productRef = 7EFF41972F17D99D00B48704 /* RIBs */; }; /* End PBXBuildFile section */ @@ -59,7 +59,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7EA3A6CE2F61EE4A00D01810 /* RIBs in Frameworks */, + 7EA3A6D62F62251D00D01810 /* RIBs in Frameworks */, 7EFF41982F17D99D00B48704 /* RIBs in Frameworks */, 7EB78D4F2F12CC0000547345 /* RIBs in Frameworks */, 7E71E73E2F4426FA002FD889 /* RIBs in Frameworks */, @@ -117,7 +117,7 @@ 7EB78D4E2F12CC0000547345 /* RIBs */, 7EFF41972F17D99D00B48704 /* RIBs */, 7E71E73D2F4426FA002FD889 /* RIBs */, - 7EA3A6CD2F61EE4A00D01810 /* RIBs */, + 7EA3A6D52F62251D00D01810 /* RIBs */, ); productName = RIBsAppExample2; productReference = 7EB78D192F12CB3800547345 /* RIBsAppExample2.app */; @@ -175,7 +175,7 @@ mainGroup = 7EB78D102F12CB3800547345; minimizedProjectReferenceProxies = 1; packageReferences = ( - 7EA3A6CC2F61EE4A00D01810 /* XCRemoteSwiftPackageReference "RIBs-iOS" */, + 7EA3A6D42F62251D00D01810 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */, ); preferredProjectObjectVersion = 77; productRefGroup = 7EB78D1A2F12CB3800547345 /* Products */; @@ -254,10 +254,10 @@ OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = minimal; - SWIFT_VERSION = 5.0; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -285,10 +285,10 @@ OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.mobileengineer.RIBsAppExample2; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = minimal; - SWIFT_VERSION = 5.0; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -484,25 +484,20 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - 7EA3A6CC2F61EE4A00D01810 /* XCRemoteSwiftPackageReference "RIBs-iOS" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/uber/RIBs-iOS.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; +/* Begin XCLocalSwiftPackageReference section */ + 7EA3A6D42F62251D00D01810 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../../RIBs-iOS"; }; -/* End XCRemoteSwiftPackageReference section */ +/* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 7E71E73D2F4426FA002FD889 /* RIBs */ = { isa = XCSwiftPackageProductDependency; productName = RIBs; }; - 7EA3A6CD2F61EE4A00D01810 /* RIBs */ = { + 7EA3A6D52F62251D00D01810 /* RIBs */ = { isa = XCSwiftPackageProductDependency; - package = 7EA3A6CC2F61EE4A00D01810 /* XCRemoteSwiftPackageReference "RIBs-iOS" */; productName = RIBs; }; 7EB78D4E2F12CC0000547345 /* RIBs */ = { From 74dab0692874dac9269c1610ab525b028e703ea1 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Thu, 12 Mar 2026 15:07:58 -0500 Subject: [PATCH 29/30] Add ComponentizedBuilder example --- .../FirstViewableRIB/AuthService.swift | 18 +++++ .../FirstViewableRIBBuilder.swift | 16 ++++- .../FirstViewableRIBInteractor.swift | 33 ++++++++- .../FirstViewableRIBRouter.swift | 30 +++++++- .../FirstViewableRIBViewController.swift | 30 ++++++-- .../HomeRIB/HomeRIBBuilder.swift | 46 +++++++++++++ .../HomeRIB/HomeRIBInteractor.swift | 48 +++++++++++++ .../HomeRIB/HomeRIBRouter.swift | 23 +++++++ .../HomeRIB/HomeRIBViewController.swift | 68 +++++++++++++++++++ .../MainRIB/CurrentUserService.swift | 19 ++++++ .../MainRIB/MainRIBBuilder.swift | 49 +++++++++++++ .../MainRIB/MainRIBInteractor.swift | 53 +++++++++++++++ .../MainRIB/MainRIBRouter.swift | 44 ++++++++++++ .../MainRIB/MainRIBViewController.swift | 22 ++++++ .../RIBsAppExample2/UserSession.swift | 12 ++++ 15 files changed, 500 insertions(+), 11 deletions(-) create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/AuthService.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBBuilder.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBInteractor.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBRouter.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/HomeRIB/HomeRIBViewController.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/CurrentUserService.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBBuilder.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBInteractor.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBRouter.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/MainRIB/MainRIBViewController.swift create mode 100644 Examples/RIBsAppExample2/RIBsAppExample2/UserSession.swift 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 index b265a28..8c23065 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBBuilder.swift @@ -12,7 +12,7 @@ protocol FirstViewableRIBDependency: Dependency { // created by this RIB. } -final class FirstViewableRIBComponent: Component, SecondViewableRIBDependency, FourthViewableRIBDependency { +final class FirstViewableRIBComponent: Component, SecondViewableRIBDependency, FourthViewableRIBDependency, MainRIBDependency { var actorService: ActorServicable { ActorService() @@ -29,6 +29,16 @@ final class FirstViewableRIBComponent: Component, Se 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 @@ -46,9 +56,9 @@ final class FirstViewableRIBBuilder: Builder, FirstV 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) + 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) + 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 index efa1bb4..7a2d37b 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBInteractor.swift @@ -16,6 +16,8 @@ protocol FirstViewableRIBRouting: ViewableRouting { func routeAwayFromSecondViewableRIB() func routeToFourthViewableRIB() -> FourthViewableRIBActionableItem func routeAwayFromFourthViewableRIB() + func routeToMainRIB(userSession: UserSession) + func routeAwayFromMainRIB() } protocol FirstViewableRIBPresentable: Presentable { @@ -34,10 +36,12 @@ final class FirstViewableRIBInteractor: PresentableInteractor.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, ())> { diff --git a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift index d9b0fa1..cf23295 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift +++ b/Examples/RIBsAppExample2/RIBsAppExample2/FirstViewableRIB/FirstViewableRIBRouter.swift @@ -7,7 +7,7 @@ import RIBs -protocol FirstViewableRIBInteractable: Interactable, FirstViewableRIBActionableItem, SecondViewableRIBListener, FourthViewableRIBListener { +protocol FirstViewableRIBInteractable: Interactable, FirstViewableRIBActionableItem, SecondViewableRIBListener, FourthViewableRIBListener, MainRIBListener { var router: FirstViewableRIBRouting? { get set } var listener: FirstViewableRIBListener? { get set } } @@ -24,9 +24,13 @@ final class FirstViewableRIBRouter: ViewableRouter { + + 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/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/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 +} From e8cb22e829d9a51c8e3fd3ea79a2f0372704c38b Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 21 Mar 2026 16:31:55 -0500 Subject: [PATCH 30/30] Add Swift 6 migration doc --- .../RIBsAppExample2.xcodeproj/project.pbxproj | 11 +- README.md | 2 + SWIFT6_STRICT_CONCURRENCY_MIGRATION.md | 138 ++++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 SWIFT6_STRICT_CONCURRENCY_MIGRATION.md diff --git a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj index bff5499..d684d1a 100644 --- a/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj +++ b/Examples/RIBsAppExample2/RIBsAppExample2.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* 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 */ @@ -59,6 +60,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7EA3A6F42F635A7100D01810 /* RIBs in Frameworks */, 7EA3A6D62F62251D00D01810 /* RIBs in Frameworks */, 7EFF41982F17D99D00B48704 /* RIBs in Frameworks */, 7EB78D4F2F12CC0000547345 /* RIBs in Frameworks */, @@ -118,6 +120,7 @@ 7EFF41972F17D99D00B48704 /* RIBs */, 7E71E73D2F4426FA002FD889 /* RIBs */, 7EA3A6D52F62251D00D01810 /* RIBs */, + 7EA3A6F32F635A7100D01810 /* RIBs */, ); productName = RIBsAppExample2; productReference = 7EB78D192F12CB3800547345 /* RIBsAppExample2.app */; @@ -175,7 +178,7 @@ mainGroup = 7EB78D102F12CB3800547345; minimizedProjectReferenceProxies = 1; packageReferences = ( - 7EA3A6D42F62251D00D01810 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */, + 7EA3A6F22F635A7100D01810 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */, ); preferredProjectObjectVersion = 77; productRefGroup = 7EB78D1A2F12CB3800547345 /* Products */; @@ -485,7 +488,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 7EA3A6D42F62251D00D01810 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */ = { + 7EA3A6F22F635A7100D01810 /* XCLocalSwiftPackageReference "../../../RIBs-iOS" */ = { isa = XCLocalSwiftPackageReference; relativePath = "../../../RIBs-iOS"; }; @@ -500,6 +503,10 @@ isa = XCSwiftPackageProductDependency; productName = RIBs; }; + 7EA3A6F32F635A7100D01810 /* RIBs */ = { + isa = XCSwiftPackageProductDependency; + productName = RIBs; + }; 7EB78D4E2F12CC0000547345 /* RIBs */ = { isa = XCSwiftPackageProductDependency; productName = RIBs; 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/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+) |