From 837069904de6088e6e87cc6420c84ed66ac25161 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 16 Aug 2025 20:10:40 -0500 Subject: [PATCH 01/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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 a4119e1df093cdef3935739a65ae507b38922ea3 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 11:37:11 -0500 Subject: [PATCH 08/33] WIP --- Package.swift | 11 +++-- RIBs/Classes/Builder.swift | 2 +- RIBs/Classes/Interactor.swift | 8 ++-- RIBs/Classes/LaunchRouter.swift | 2 + RIBs/Classes/LeakDetector/LeakDetector.swift | 42 ++++++++++---------- RIBs/Classes/PresentableInteractor.swift | 2 +- RIBs/Classes/Presenter.swift | 13 +++++- 7 files changed, 48 insertions(+), 32 deletions(-) diff --git a/Package.swift b/Package.swift index 53d3f9b..60f6343 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,10 @@ -// swift-tools-version:5.5 +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "RIBs", platforms: [ - .iOS("15.0"), + .iOS(.v18) ], products: [ .library(name: "RIBs", targets: ["RIBs"]), @@ -20,7 +20,12 @@ let package = Package( .product(name: "RxSwift", package: "RxSwift"), .product(name: "RxRelay", package: "RxSwift") ], - path: "RIBs" + path: "RIBs", + swiftSettings: [ +// .unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"]), + .swiftLanguageMode(.v6), + .enableUpcomingFeature("StrictConcurrency"), + ] ), .testTarget( name: "RIBsTests", diff --git a/RIBs/Classes/Builder.swift b/RIBs/Classes/Builder.swift index 9f16e9d..51f2300 100644 --- a/RIBs/Classes/Builder.swift +++ b/RIBs/Classes/Builder.swift @@ -17,7 +17,7 @@ import Foundation /// The base builder protocol that all builders should conform to. -public protocol Buildable: AnyObject {} +nonisolated public protocol Buildable: AnyObject {} /// Utility that instantiates a RIB and sets up its internal wirings. open class Builder: Buildable { diff --git a/RIBs/Classes/Interactor.swift b/RIBs/Classes/Interactor.swift index d80ded2..f06add0 100644 --- a/RIBs/Classes/Interactor.swift +++ b/RIBs/Classes/Interactor.swift @@ -16,10 +16,9 @@ import Foundation import RxSwift -import UIKit /// Protocol defining the activeness of an interactor's scope. -public protocol InteractorScope: AnyObject { +nonisolated public protocol InteractorScope: AnyObject { // The following properties must be declared in the base protocol, since `Router` internally invokes these methods. // In order to unit test router with a mock interactor, the mocked interactor first needs to conform to the custom @@ -37,7 +36,7 @@ public protocol InteractorScope: AnyObject { } /// The base protocol for all interactors. -public protocol Interactable: InteractorScope { +nonisolated public protocol Interactable: InteractorScope { // The following methods must be declared in the base protocol, since `Router` internally invokes these methods. // In order to unit test router with a mock interactor, the mocked interactor first needs to conform to the custom @@ -64,7 +63,8 @@ public protocol Interactable: InteractorScope { /// active. /// /// An `Interactor` should only perform its business logic when it's currently active. -open class Interactor: Interactable { +nonisolated open class Interactor: Interactable { + /// Indicates if the interactor is active. public final var isActive: Bool { diff --git a/RIBs/Classes/LaunchRouter.swift b/RIBs/Classes/LaunchRouter.swift index d50dc20..b570c44 100644 --- a/RIBs/Classes/LaunchRouter.swift +++ b/RIBs/Classes/LaunchRouter.swift @@ -22,6 +22,7 @@ public protocol LaunchRouting: ViewableRouting { /// Launches the router tree. /// /// - parameter window: The application window to launch from. + @MainActor func launch(from window: UIWindow) } @@ -39,6 +40,7 @@ open class LaunchRouter: ViewableRouter: Interactor { +nonisolated open class PresentableInteractor: Interactor { /// The `Presenter` associated with this `Interactor`. public let presenter: PresenterType diff --git a/RIBs/Classes/Presenter.swift b/RIBs/Classes/Presenter.swift index edc1d4f..6944c07 100644 --- a/RIBs/Classes/Presenter.swift +++ b/RIBs/Classes/Presenter.swift @@ -17,12 +17,21 @@ import Foundation /// The base protocol for all `Presenter`s. -public protocol Presentable: AnyObject {} +@MainActor +public protocol Presentable: AnyObject { + associatedtype Listener + + nonisolated var listener: Listener? { get set } +} /// 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. -open class Presenter: Presentable { +open class Presenter: @MainActor Presentable { + nonisolated(unsafe) public var listener: Listener? + + public typealias Listener = Any + /// The view controller of this presenter. public let viewController: ViewControllerType From 64e6b77864a45aac88859790e8a59a152b1dd815 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 12:44:03 -0500 Subject: [PATCH 09/33] Add a convenience method to run presenter calls on the main thread --- RIBs/Classes/PresentableInteractor.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index b257664..3bca352 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -30,6 +30,14 @@ nonisolated open class PresentableInteractor: Interactor { public init(presenter: PresenterType) { self.presenter = presenter } + + public nonisolated func presentOnMainThread(_ block: @escaping @MainActor (PresenterType) -> Void) { + nonisolated(unsafe) let presenter = self.presenter + + Task { @MainActor in + block(presenter) + } + } // MARK: - Private From 284e7a3ac013c58e4909c8ffc160ca122d1c013a Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 12:55:38 -0500 Subject: [PATCH 10/33] Fix the tests --- RIBsTests/LaunchRouterTests.swift | 1 + RIBsTests/Mocks.swift | 1 + RIBsTests/Router/ViewableRouterTests.swift | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) 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/Mocks.swift b/RIBsTests/Mocks.swift index 08fe1bd..a95ea62 100644 --- a/RIBsTests/Mocks.swift +++ b/RIBsTests/Mocks.swift @@ -40,6 +40,7 @@ class WindowMock: UIWindow { private var internalRootViewController: UIViewController? } +@MainActor class ViewControllableMock: ViewControllable { let uiviewController = UIViewController(nibName: nil, bundle: nil) } diff --git a/RIBsTests/Router/ViewableRouterTests.swift b/RIBsTests/Router/ViewableRouterTests.swift index 88d3f0e..5710c15 100644 --- a/RIBsTests/Router/ViewableRouterTests.swift +++ b/RIBsTests/Router/ViewableRouterTests.swift @@ -11,7 +11,8 @@ import XCTest import CwlPreconditionTesting -final class ViewControllerMock: ViewControllable { +@MainActor +final class ViewControllerMock: @MainActor ViewControllable { var uiviewController: UIViewController { return UIViewController() From 899d529a87dbb74016a6ee8400f02da4a83d250f Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 15:30:53 -0500 Subject: [PATCH 11/33] Make the tests pass --- Example/RIBs.xcodeproj/project.pbxproj | 2 ++ RIBs/Classes/LeakDetector/LeakDetector.swift | 1 - RIBsTests/LaunchRouterTests.swift | 4 ++-- RIBsTests/Router/ViewableRouterTests.swift | 10 +++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Example/RIBs.xcodeproj/project.pbxproj b/Example/RIBs.xcodeproj/project.pbxproj index 45519b9..aeea937 100644 --- a/Example/RIBs.xcodeproj/project.pbxproj +++ b/Example/RIBs.xcodeproj/project.pbxproj @@ -382,6 +382,7 @@ MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; @@ -398,6 +399,7 @@ MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; diff --git a/RIBs/Classes/LeakDetector/LeakDetector.swift b/RIBs/Classes/LeakDetector/LeakDetector.swift index 75cb1cb..aae44e2 100644 --- a/RIBs/Classes/LeakDetector/LeakDetector.swift +++ b/RIBs/Classes/LeakDetector/LeakDetector.swift @@ -83,7 +83,6 @@ public class LeakDetector { public func expectDeallocate(object: AnyObject, inTime time: TimeInterval = LeakDefaultExpectationTime.deallocation) -> LeakDetectionHandle { expectationCount.accept(expectationCount.value + 1) - let objectDescription = String(describing: object) let objectId = String(ObjectIdentifier(object).hashValue) as NSString trackingObjects.setObject(object, forKey: objectId) diff --git a/RIBsTests/LaunchRouterTests.swift b/RIBsTests/LaunchRouterTests.swift index c8d311d..e35876c 100644 --- a/RIBsTests/LaunchRouterTests.swift +++ b/RIBsTests/LaunchRouterTests.swift @@ -27,8 +27,8 @@ final class LaunchRouterTests: XCTestCase { // MARK: - Setup - override func setUp() { - super.setUp() + override func setUp() async throws { + try await super.setUp() interactor = InteractableMock() viewController = ViewControllableMock() diff --git a/RIBsTests/Router/ViewableRouterTests.swift b/RIBsTests/Router/ViewableRouterTests.swift index 5710c15..049f8e7 100644 --- a/RIBsTests/Router/ViewableRouterTests.swift +++ b/RIBsTests/Router/ViewableRouterTests.swift @@ -12,7 +12,7 @@ import CwlPreconditionTesting @MainActor -final class ViewControllerMock: @MainActor ViewControllable { +final class ViewControllerMock: ViewControllable { var uiviewController: UIViewController { return UIViewController() @@ -31,10 +31,10 @@ final class ViewableRouterTests: XCTestCase { LeakDetector.setInstance(leakDetectorMock) } - func test_leakDetection() { + func test_leakDetection() async { // given let interactor = PresentableInteractor(presenter: PresenterMock()) - let viewController = ViewControllerMock() + let viewController = await ViewControllerMock() router = ViewableRouter(interactor: interactor, viewController: viewController) router.load() // when @@ -43,10 +43,10 @@ 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() + let viewController = await ViewControllerMock() router = ViewableRouter(interactor: interactor, viewController: viewController) router.load() // when From 92abbd5edae31f6e8904c51716d988be2e722986 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 16:01:05 -0500 Subject: [PATCH 12/33] More progress on implementing isolation in viewable router and view controllable --- RIBs/Classes/Router.swift | 6 +++--- RIBs/Classes/ViewControllable.swift | 1 + RIBs/Classes/ViewableRouter.swift | 29 ++++++++++++++++++----------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/RIBs/Classes/Router.swift b/RIBs/Classes/Router.swift index de593d0..b678831 100644 --- a/RIBs/Classes/Router.swift +++ b/RIBs/Classes/Router.swift @@ -24,7 +24,7 @@ public enum RouterLifecycle { } /// The scope of a `Router`, defining various lifecycles of a `Router`. -public protocol RouterScope: AnyObject { +nonisolated public protocol RouterScope: AnyObject { /// An observable that emits values when the router scope reaches its corresponding life-cycle stages. This /// observable completes when the router scope is deallocated. @@ -32,7 +32,7 @@ public protocol RouterScope: AnyObject { } /// The base protocol for all routers. -public protocol Routing: RouterScope { +nonisolated public protocol Routing: RouterScope { // The following methods must be declared in the base protocol, since `Router` internally invokes these methods. // In order to unit test router with a mock child router, the mocked child router first needs to conform to the @@ -73,7 +73,7 @@ public protocol Routing: RouterScope { /// Router drives the lifecycle of its owned `Interactor`. /// /// Routers should always use helper builders to instantiate children routers. -open class Router: Routing { +nonisolated open class Router: Routing { /// The corresponding `Interactor` owned by this `Router`. public let interactor: InteractorType diff --git a/RIBs/Classes/ViewControllable.swift b/RIBs/Classes/ViewControllable.swift index 0e27040..211cf31 100644 --- a/RIBs/Classes/ViewControllable.swift +++ b/RIBs/Classes/ViewControllable.swift @@ -17,6 +17,7 @@ import UIKit /// Basic interface between a `Router` and the UIKit `UIViewController`. +@MainActor public protocol ViewControllable: AnyObject { var uiviewController: UIViewController { get } diff --git a/RIBs/Classes/ViewableRouter.swift b/RIBs/Classes/ViewableRouter.swift index 82488b4..8645aa4 100644 --- a/RIBs/Classes/ViewableRouter.swift +++ b/RIBs/Classes/ViewableRouter.swift @@ -68,28 +68,35 @@ open class ViewableRouter: Router Date: Sat, 11 Oct 2025 16:28:40 -0500 Subject: [PATCH 13/33] Mark builder and component as nonisolated --- RIBs/Classes/Builder.swift | 2 +- RIBs/Classes/DI/Component.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RIBs/Classes/Builder.swift b/RIBs/Classes/Builder.swift index 51f2300..d87c02f 100644 --- a/RIBs/Classes/Builder.swift +++ b/RIBs/Classes/Builder.swift @@ -20,7 +20,7 @@ import Foundation nonisolated public protocol Buildable: AnyObject {} /// Utility that instantiates a RIB and sets up its internal wirings. -open class Builder: Buildable { +nonisolated open class Builder: Buildable { /// The dependency used for this builder to build the RIB. public let dependency: DependencyType diff --git a/RIBs/Classes/DI/Component.swift b/RIBs/Classes/DI/Component.swift index 4a178ff..4a3663c 100644 --- a/RIBs/Classes/DI/Component.swift +++ b/RIBs/Classes/DI/Component.swift @@ -23,7 +23,7 @@ import Foundation /// /// A component subclass implementation should conform to child 'Dependency' protocols, defined by all of its immediate /// children. -open class Component: Dependency { +nonisolated open class Component: Dependency { /// The dependency of this `Component`. public let dependency: DependencyType From d122d9ac0891ed02d585de843750f342ca7eb699 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 17:16:47 -0500 Subject: [PATCH 14/33] Put back the leak detector code --- RIBs/Classes/LeakDetector/LeakDetector.swift | 39 ++++++++++---------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/RIBs/Classes/LeakDetector/LeakDetector.swift b/RIBs/Classes/LeakDetector/LeakDetector.swift index aae44e2..0495daa 100644 --- a/RIBs/Classes/LeakDetector/LeakDetector.swift +++ b/RIBs/Classes/LeakDetector/LeakDetector.swift @@ -83,6 +83,7 @@ public class LeakDetector { public func expectDeallocate(object: AnyObject, inTime time: TimeInterval = LeakDefaultExpectationTime.deallocation) -> LeakDetectionHandle { expectationCount.accept(expectationCount.value + 1) + let objectDescription = String(describing: object) let objectId = String(ObjectIdentifier(object).hashValue) as NSString trackingObjects.setObject(object, forKey: objectId) @@ -90,25 +91,25 @@ public class LeakDetector { self.expectationCount.accept(self.expectationCount.value - 1) } -// Executor.execute(withDelay: time) { -// // Retain the handle so we can check for the cancelled status. Also cannot use the cancellable -// // concurrency API since the returned handle must be retained to ensure closure is executed. -// if !handle.cancelled { -// let didDeallocate = (self.trackingObjects.object(forKey: objectId) == nil) -// let message = "<\(objectDescription): \(objectId)> has leaked. Objects are expected to be deallocated at this time: \(self.trackingObjects)" -// -// if self.disableLeakDetector { -// if !didDeallocate { -// print("Leak detection is disabled. This should only be used for debugging purposes.") -// print(message) -// } -// } else { -// assert(didDeallocate, message) -// } -// } -// -// self.expectationCount.accept(self.expectationCount.value - 1) -// } + Executor.execute(withDelay: time) { + // Retain the handle so we can check for the cancelled status. Also cannot use the cancellable + // concurrency API since the returned handle must be retained to ensure closure is executed. + if !handle.cancelled { + let didDeallocate = (self.trackingObjects.object(forKey: objectId) == nil) + let message = "<\(objectDescription): \(objectId)> has leaked. Objects are expected to be deallocated at this time: \(self.trackingObjects)" + + if self.disableLeakDetector { + if !didDeallocate { + print("Leak detection is disabled. This should only be used for debugging purposes.") + print(message) + } + } else { + assert(didDeallocate, message) + } + } + + self.expectationCount.accept(self.expectationCount.value - 1) + } return handle } From 29e501a6e1e59166ec183e60d609ee3f6dab238b Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 19:23:32 -0500 Subject: [PATCH 15/33] Fix tests for new concurrency --- RIBs/Classes/PresentableInteractor.swift | 2 +- RIBs/Classes/ViewableRouter.swift | 5 ++- RIBsTests/LeakDetector/LeakDetectorMock.swift | 44 ++++++++++++++----- RIBsTests/Router/ViewableRouterTests.swift | 35 ++++++++++++--- 4 files changed, 66 insertions(+), 20 deletions(-) diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index 3bca352..81b02fe 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -31,7 +31,7 @@ nonisolated open class PresentableInteractor: Interactor { self.presenter = presenter } - public nonisolated func presentOnMainThread(_ block: @escaping @MainActor (PresenterType) -> Void) { + public nonisolated func presentOnMainThread(_ block: @escaping @MainActor (_ presenter: PresenterType) -> Void) { nonisolated(unsafe) let presenter = self.presenter Task { @MainActor in diff --git a/RIBs/Classes/ViewableRouter.swift b/RIBs/Classes/ViewableRouter.swift index 8645aa4..1d9a3d5 100644 --- a/RIBs/Classes/ViewableRouter.swift +++ b/RIBs/Classes/ViewableRouter.swift @@ -94,9 +94,10 @@ open class ViewableRouter: Router { + // Note: The get block for a computed property is synchronous. + queue.sync { + self._statusCallCount += 1 + } + return super.status + } + + + var onDeallocateCalled: (() -> Void)? + private var _expectDeallocateCallCount = 0 + var expectDeallocateCallCount: Int { + queue.sync { self._expectDeallocateCallCount } + } override func expectDeallocate(object: AnyObject, inTime time: TimeInterval) -> LeakDetectionHandle { - expectDeallocateCallCount += 1 + queue.sync { + self._expectDeallocateCallCount += 1 + } + onDeallocateCalled?() return LeakDetectionHandleMock() } - var expectViewControllerDisappearCallCount = 0 + + var onViewControllerDisappearCalled: (() -> Void)? + private var _expectViewControllerDisappearCallCount = 0 + var expectViewControllerDisappearCallCount: Int { + queue.sync { self._expectViewControllerDisappearCallCount } + } override func expectViewControllerDisappear(viewController: UIViewController, inTime time: TimeInterval) -> LeakDetectionHandle { - expectViewControllerDisappearCallCount += 1 + queue.sync { + self._expectViewControllerDisappearCallCount += 1 + } + onViewControllerDisappearCalled?() return LeakDetectionHandleMock() } - - var statusCallCount = 0 - override var status: Observable { - statusCallCount += 1 - return super.status - } } diff --git a/RIBsTests/Router/ViewableRouterTests.swift b/RIBsTests/Router/ViewableRouterTests.swift index 049f8e7..6b5caeb 100644 --- a/RIBsTests/Router/ViewableRouterTests.swift +++ b/RIBsTests/Router/ViewableRouterTests.swift @@ -31,27 +31,48 @@ final class ViewableRouterTests: XCTestCase { LeakDetector.setInstance(leakDetectorMock) } - func test_leakDetection() async { + func test_leakDetection() { // given let interactor = PresentableInteractor(presenter: PresenterMock()) - let viewController = await ViewControllerMock() + let viewController = ViewControllerMock() router = ViewableRouter(interactor: interactor, viewController: viewController) router.load() + + let disappearExpectation = self.expectation(description: "Wait for view controller to disappear") + + leakDetectorMock.onViewControllerDisappearCalled = { [weak leakDetectorMock] in + if leakDetectorMock?.expectViewControllerDisappearCallCount == 1 { + disappearExpectation.fulfill() + } + } + // when interactor.deactivate() + // then - XCTAssertEqual(leakDetectorMock.expectViewControllerDisappearCallCount, 1) + wait(for: [disappearExpectation], timeout: 2.0) } - - func test_deinit_triggers_leakDetection() async { + + func test_deinit_triggers_leakDetection() { // given let interactor = PresentableInteractor(presenter: PresenterMock()) - let viewController = await ViewControllerMock() + let viewController = ViewControllerMock() router = ViewableRouter(interactor: interactor, viewController: viewController) router.load() + // when + let deallocationExpectation = self.expectation(description: "Expect deallocate to be called twice") + + leakDetectorMock.onDeallocateCalled = { [weak leakDetectorMock] in + + if leakDetectorMock?.expectDeallocateCallCount == 2 { + deallocationExpectation.fulfill() + } + } + // when router = nil + // then - XCTAssertEqual(leakDetectorMock.expectDeallocateCallCount, 2) + wait(for: [deallocationExpectation], timeout: 5.0) } } From 17ce9da11a56048fa26285d1c8d2b7689f0f0aca Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 20:05:58 -0500 Subject: [PATCH 16/33] Make presenatble sendable --- RIBs/Classes/Presenter.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RIBs/Classes/Presenter.swift b/RIBs/Classes/Presenter.swift index 6944c07..ce3ef9d 100644 --- a/RIBs/Classes/Presenter.swift +++ b/RIBs/Classes/Presenter.swift @@ -18,7 +18,7 @@ import Foundation /// The base protocol for all `Presenter`s. @MainActor -public protocol Presentable: AnyObject { +public protocol Presentable: AnyObject, Sendable { associatedtype Listener nonisolated var listener: Listener? { get set } @@ -27,7 +27,7 @@ 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. -open class Presenter: @MainActor Presentable { +open class Presenter: Presentable { nonisolated(unsafe) public var listener: Listener? public typealias Listener = Any From aa80f488b798a193bbde80e633b3fe7f7ea9b83b Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 18 Oct 2025 13:28:39 -0500 Subject: [PATCH 17/33] Make presentable interactor an unchecked sendable --- RIBs/Classes/PresentableInteractor.swift | 2 +- RIBs/Classes/Presenter.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index 81b02fe..4429d16 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -17,7 +17,7 @@ import Foundation /// Base class of an `Interactor` that actually has an associated `Presenter` and `View`. -nonisolated open class PresentableInteractor: Interactor { +nonisolated open class PresentableInteractor: Interactor { /// The `Presenter` associated with this `Interactor`. public let presenter: PresenterType diff --git a/RIBs/Classes/Presenter.swift b/RIBs/Classes/Presenter.swift index ce3ef9d..27b8653 100644 --- a/RIBs/Classes/Presenter.swift +++ b/RIBs/Classes/Presenter.swift @@ -18,7 +18,7 @@ import Foundation /// The base protocol for all `Presenter`s. @MainActor -public protocol Presentable: AnyObject, Sendable { +public protocol Presentable: AnyObject & SendableMetatype { associatedtype Listener nonisolated var listener: Listener? { get set } @@ -28,9 +28,9 @@ public protocol Presentable: AnyObject, Sendable { /// `ViewController` can consume and display. It also maps UI events to business logic method, invoked to /// its listener. open class Presenter: Presentable { - nonisolated(unsafe) public var listener: Listener? + nonisolated(unsafe) public weak var listener: Listener? - public typealias Listener = Any + public typealias Listener = AnyObject /// The view controller of this presenter. From 49adffb2e88a436791bdc3a4036be4f8968bb86f Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 18 Oct 2025 17:08:27 -0500 Subject: [PATCH 18/33] Make interacor @unchecked Sendable - add helper method for UI navigation to ViewableRouter - add docs to presenter helper method and to the viewable router helper methods --- RIBs/Classes/Interactor.swift | 2 +- RIBs/Classes/PresentableInteractor.swift | 42 +++++++++++++ RIBs/Classes/ViewableRouter.swift | 75 ++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/RIBs/Classes/Interactor.swift b/RIBs/Classes/Interactor.swift index f06add0..635a365 100644 --- a/RIBs/Classes/Interactor.swift +++ b/RIBs/Classes/Interactor.swift @@ -63,7 +63,7 @@ nonisolated public protocol Interactable: InteractorScope { /// active. /// /// An `Interactor` should only perform its business logic when it's currently active. -nonisolated open class Interactor: Interactable { +nonisolated open class Interactor: Interactable, @unchecked Sendable { /// Indicates if the interactor is active. diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index 4429d16..d0931ac 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -31,6 +31,48 @@ nonisolated open class PresentableInteractor Void) { nonisolated(unsafe) let presenter = self.presenter diff --git a/RIBs/Classes/ViewableRouter.swift b/RIBs/Classes/ViewableRouter.swift index 1d9a3d5..2e998fb 100644 --- a/RIBs/Classes/ViewableRouter.swift +++ b/RIBs/Classes/ViewableRouter.swift @@ -54,7 +54,82 @@ open class ViewableRouter: Router Void) { + nonisolated(unsafe) let thisRouterViewController = self.viewController + Task { @MainActor in + block(thisRouterViewController) + } + } + + /// A helper method to safely call viewController methods for UI navigation on the main thread with a child view controller. + /// This overload provides both the current router's view controller and a child view controller to the closure, + /// ensuring all UI operations happen on the main thread. + /// + /// When routing in RIBs that have UI (using ViewableRouter), the plumbing of routing such as clearing out + /// child RIB references, setting them up, or attaching/detaching child routers can happen on any thread. + /// However, the actual physical/mechanical navigation of the UI - such as pushing or popping child RIB's UI + /// onto/off the navigation controller stack, modally presenting or dismissing it, or attaching/detaching it + /// from the UI hierarchy via custom implementation using child containment API - must all be done on the main thread + /// since they manipulate the UI tree and trigger UI rendering. + /// + /// This method ensures that your viewController method calls run on the main thread. + /// Use this method when you encounter compiler warnings such as: + /// "Main actor-isolated property 'uiviewController' cannot be referenced from a nonisolated context" + /// + /// This is an overload of the basic `navigateOnMainThread(_:)` method that includes a child view controller parameter. + /// Use this method when you need to perform UI navigation that involves both the current router's view controller + /// and a child view controller, such as presenting, pushing, or custom containment operations. + /// For navigation operations that only involve the current router's view controller, use the basic `navigateOnMainThread(_:)` method instead. + /// + /// Example usage - routing to child RIB: + /// ```swift + /// func routeToChildRIB() { + /// let childRIBRouter = childRIBBuilder.build(withListener: interactor) + /// self.childRIBRouter = childRIBRouter + /// navigateOnMainThread(with: childRIBRouter.viewControllable) { thisRouterViewController, childViewController in + /// thisRouterViewController.attachChildRIBViewController(childViewController.uiviewController) + /// } + /// attachChild(childRIBRouter) + /// } + /// ``` + + public nonisolated func navigateOnMainThread(with childViewController: ViewControllable, _ block: @escaping @MainActor (_ thisRouterViewController: ViewControllerType, _ childViewController: ViewControllable) -> Void) { + nonisolated(unsafe) let thisRouterViewController = self.viewController + nonisolated(unsafe) let childVC = childViewController + + Task { @MainActor in + block(thisRouterViewController, childVC) + } + } + // MARK: - Internal override func internalDidLoad() { From afa08eb1d248d839dde6efe8e9718d59fecd3c0e Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 16 Aug 2025 20:10:40 -0500 Subject: [PATCH 19/33] 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 20/33] 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 21/33] 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 91f2c43a1c97bb830faf4eb9bd35e6f95579782b Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 11:37:11 -0500 Subject: [PATCH 22/33] WIP --- Package.swift | 11 +++-- RIBs/Classes/Builder.swift | 2 +- RIBs/Classes/Interactor.swift | 8 ++-- RIBs/Classes/LaunchRouter.swift | 2 + RIBs/Classes/LeakDetector/LeakDetector.swift | 42 ++++++++++---------- RIBs/Classes/PresentableInteractor.swift | 2 +- RIBs/Classes/Presenter.swift | 13 +++++- 7 files changed, 48 insertions(+), 32 deletions(-) diff --git a/Package.swift b/Package.swift index 53d3f9b..60f6343 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,10 @@ -// swift-tools-version:5.5 +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "RIBs", platforms: [ - .iOS("15.0"), + .iOS(.v18) ], products: [ .library(name: "RIBs", targets: ["RIBs"]), @@ -20,7 +20,12 @@ let package = Package( .product(name: "RxSwift", package: "RxSwift"), .product(name: "RxRelay", package: "RxSwift") ], - path: "RIBs" + path: "RIBs", + swiftSettings: [ +// .unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"]), + .swiftLanguageMode(.v6), + .enableUpcomingFeature("StrictConcurrency"), + ] ), .testTarget( name: "RIBsTests", diff --git a/RIBs/Classes/Builder.swift b/RIBs/Classes/Builder.swift index 9f16e9d..51f2300 100644 --- a/RIBs/Classes/Builder.swift +++ b/RIBs/Classes/Builder.swift @@ -17,7 +17,7 @@ import Foundation /// The base builder protocol that all builders should conform to. -public protocol Buildable: AnyObject {} +nonisolated public protocol Buildable: AnyObject {} /// Utility that instantiates a RIB and sets up its internal wirings. open class Builder: Buildable { diff --git a/RIBs/Classes/Interactor.swift b/RIBs/Classes/Interactor.swift index d80ded2..f06add0 100644 --- a/RIBs/Classes/Interactor.swift +++ b/RIBs/Classes/Interactor.swift @@ -16,10 +16,9 @@ import Foundation import RxSwift -import UIKit /// Protocol defining the activeness of an interactor's scope. -public protocol InteractorScope: AnyObject { +nonisolated public protocol InteractorScope: AnyObject { // The following properties must be declared in the base protocol, since `Router` internally invokes these methods. // In order to unit test router with a mock interactor, the mocked interactor first needs to conform to the custom @@ -37,7 +36,7 @@ public protocol InteractorScope: AnyObject { } /// The base protocol for all interactors. -public protocol Interactable: InteractorScope { +nonisolated public protocol Interactable: InteractorScope { // The following methods must be declared in the base protocol, since `Router` internally invokes these methods. // In order to unit test router with a mock interactor, the mocked interactor first needs to conform to the custom @@ -64,7 +63,8 @@ public protocol Interactable: InteractorScope { /// active. /// /// An `Interactor` should only perform its business logic when it's currently active. -open class Interactor: Interactable { +nonisolated open class Interactor: Interactable { + /// Indicates if the interactor is active. public final var isActive: Bool { diff --git a/RIBs/Classes/LaunchRouter.swift b/RIBs/Classes/LaunchRouter.swift index d50dc20..b570c44 100644 --- a/RIBs/Classes/LaunchRouter.swift +++ b/RIBs/Classes/LaunchRouter.swift @@ -22,6 +22,7 @@ public protocol LaunchRouting: ViewableRouting { /// Launches the router tree. /// /// - parameter window: The application window to launch from. + @MainActor func launch(from window: UIWindow) } @@ -39,6 +40,7 @@ open class LaunchRouter: ViewableRouter: Interactor { +nonisolated open class PresentableInteractor: Interactor { /// The `Presenter` associated with this `Interactor`. public let presenter: PresenterType diff --git a/RIBs/Classes/Presenter.swift b/RIBs/Classes/Presenter.swift index edc1d4f..6944c07 100644 --- a/RIBs/Classes/Presenter.swift +++ b/RIBs/Classes/Presenter.swift @@ -17,12 +17,21 @@ import Foundation /// The base protocol for all `Presenter`s. -public protocol Presentable: AnyObject {} +@MainActor +public protocol Presentable: AnyObject { + associatedtype Listener + + nonisolated var listener: Listener? { get set } +} /// 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. -open class Presenter: Presentable { +open class Presenter: @MainActor Presentable { + nonisolated(unsafe) public var listener: Listener? + + public typealias Listener = Any + /// The view controller of this presenter. public let viewController: ViewControllerType From e8793678a62c55a5b41f3f2e82e58dcb192ccc41 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 12:44:03 -0500 Subject: [PATCH 23/33] Add a convenience method to run presenter calls on the main thread --- RIBs/Classes/PresentableInteractor.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index b257664..3bca352 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -30,6 +30,14 @@ nonisolated open class PresentableInteractor: Interactor { public init(presenter: PresenterType) { self.presenter = presenter } + + public nonisolated func presentOnMainThread(_ block: @escaping @MainActor (PresenterType) -> Void) { + nonisolated(unsafe) let presenter = self.presenter + + Task { @MainActor in + block(presenter) + } + } // MARK: - Private From 9fb3d677b1021c019df352478195d7e3ab5e4a97 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 12:55:38 -0500 Subject: [PATCH 24/33] Fix the tests --- RIBsTests/LaunchRouterTests.swift | 1 + RIBsTests/Mocks.swift | 1 + RIBsTests/Router/ViewableRouterTests.swift | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) 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/Mocks.swift b/RIBsTests/Mocks.swift index 08fe1bd..a95ea62 100644 --- a/RIBsTests/Mocks.swift +++ b/RIBsTests/Mocks.swift @@ -40,6 +40,7 @@ class WindowMock: UIWindow { private var internalRootViewController: UIViewController? } +@MainActor class ViewControllableMock: ViewControllable { let uiviewController = UIViewController(nibName: nil, bundle: nil) } diff --git a/RIBsTests/Router/ViewableRouterTests.swift b/RIBsTests/Router/ViewableRouterTests.swift index 88d3f0e..5710c15 100644 --- a/RIBsTests/Router/ViewableRouterTests.swift +++ b/RIBsTests/Router/ViewableRouterTests.swift @@ -11,7 +11,8 @@ import XCTest import CwlPreconditionTesting -final class ViewControllerMock: ViewControllable { +@MainActor +final class ViewControllerMock: @MainActor ViewControllable { var uiviewController: UIViewController { return UIViewController() From a2c4b88881281159776d314b5ffd651472e385f4 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 15:30:53 -0500 Subject: [PATCH 25/33] Make the tests pass --- Example/RIBs.xcodeproj/project.pbxproj | 2 ++ RIBs/Classes/LeakDetector/LeakDetector.swift | 1 - RIBsTests/LaunchRouterTests.swift | 4 ++-- RIBsTests/Router/ViewableRouterTests.swift | 10 +++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Example/RIBs.xcodeproj/project.pbxproj b/Example/RIBs.xcodeproj/project.pbxproj index 45519b9..aeea937 100644 --- a/Example/RIBs.xcodeproj/project.pbxproj +++ b/Example/RIBs.xcodeproj/project.pbxproj @@ -382,6 +382,7 @@ MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; @@ -398,6 +399,7 @@ MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; diff --git a/RIBs/Classes/LeakDetector/LeakDetector.swift b/RIBs/Classes/LeakDetector/LeakDetector.swift index 75cb1cb..aae44e2 100644 --- a/RIBs/Classes/LeakDetector/LeakDetector.swift +++ b/RIBs/Classes/LeakDetector/LeakDetector.swift @@ -83,7 +83,6 @@ public class LeakDetector { public func expectDeallocate(object: AnyObject, inTime time: TimeInterval = LeakDefaultExpectationTime.deallocation) -> LeakDetectionHandle { expectationCount.accept(expectationCount.value + 1) - let objectDescription = String(describing: object) let objectId = String(ObjectIdentifier(object).hashValue) as NSString trackingObjects.setObject(object, forKey: objectId) diff --git a/RIBsTests/LaunchRouterTests.swift b/RIBsTests/LaunchRouterTests.swift index c8d311d..e35876c 100644 --- a/RIBsTests/LaunchRouterTests.swift +++ b/RIBsTests/LaunchRouterTests.swift @@ -27,8 +27,8 @@ final class LaunchRouterTests: XCTestCase { // MARK: - Setup - override func setUp() { - super.setUp() + override func setUp() async throws { + try await super.setUp() interactor = InteractableMock() viewController = ViewControllableMock() diff --git a/RIBsTests/Router/ViewableRouterTests.swift b/RIBsTests/Router/ViewableRouterTests.swift index 5710c15..049f8e7 100644 --- a/RIBsTests/Router/ViewableRouterTests.swift +++ b/RIBsTests/Router/ViewableRouterTests.swift @@ -12,7 +12,7 @@ import CwlPreconditionTesting @MainActor -final class ViewControllerMock: @MainActor ViewControllable { +final class ViewControllerMock: ViewControllable { var uiviewController: UIViewController { return UIViewController() @@ -31,10 +31,10 @@ final class ViewableRouterTests: XCTestCase { LeakDetector.setInstance(leakDetectorMock) } - func test_leakDetection() { + func test_leakDetection() async { // given let interactor = PresentableInteractor(presenter: PresenterMock()) - let viewController = ViewControllerMock() + let viewController = await ViewControllerMock() router = ViewableRouter(interactor: interactor, viewController: viewController) router.load() // when @@ -43,10 +43,10 @@ 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() + let viewController = await ViewControllerMock() router = ViewableRouter(interactor: interactor, viewController: viewController) router.load() // when From 9cede06bc3e8e2eb39acd386b67fbab8cd7eedd1 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 16:01:05 -0500 Subject: [PATCH 26/33] More progress on implementing isolation in viewable router and view controllable --- RIBs/Classes/Router.swift | 6 +++--- RIBs/Classes/ViewControllable.swift | 1 + RIBs/Classes/ViewableRouter.swift | 29 ++++++++++++++++++----------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/RIBs/Classes/Router.swift b/RIBs/Classes/Router.swift index de593d0..b678831 100644 --- a/RIBs/Classes/Router.swift +++ b/RIBs/Classes/Router.swift @@ -24,7 +24,7 @@ public enum RouterLifecycle { } /// The scope of a `Router`, defining various lifecycles of a `Router`. -public protocol RouterScope: AnyObject { +nonisolated public protocol RouterScope: AnyObject { /// An observable that emits values when the router scope reaches its corresponding life-cycle stages. This /// observable completes when the router scope is deallocated. @@ -32,7 +32,7 @@ public protocol RouterScope: AnyObject { } /// The base protocol for all routers. -public protocol Routing: RouterScope { +nonisolated public protocol Routing: RouterScope { // The following methods must be declared in the base protocol, since `Router` internally invokes these methods. // In order to unit test router with a mock child router, the mocked child router first needs to conform to the @@ -73,7 +73,7 @@ public protocol Routing: RouterScope { /// Router drives the lifecycle of its owned `Interactor`. /// /// Routers should always use helper builders to instantiate children routers. -open class Router: Routing { +nonisolated open class Router: Routing { /// The corresponding `Interactor` owned by this `Router`. public let interactor: InteractorType diff --git a/RIBs/Classes/ViewControllable.swift b/RIBs/Classes/ViewControllable.swift index 0e27040..211cf31 100644 --- a/RIBs/Classes/ViewControllable.swift +++ b/RIBs/Classes/ViewControllable.swift @@ -17,6 +17,7 @@ import UIKit /// Basic interface between a `Router` and the UIKit `UIViewController`. +@MainActor public protocol ViewControllable: AnyObject { var uiviewController: UIViewController { get } diff --git a/RIBs/Classes/ViewableRouter.swift b/RIBs/Classes/ViewableRouter.swift index 82488b4..8645aa4 100644 --- a/RIBs/Classes/ViewableRouter.swift +++ b/RIBs/Classes/ViewableRouter.swift @@ -68,28 +68,35 @@ open class ViewableRouter: Router Date: Sat, 11 Oct 2025 16:28:40 -0500 Subject: [PATCH 27/33] Mark builder and component as nonisolated --- RIBs/Classes/Builder.swift | 2 +- RIBs/Classes/DI/Component.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RIBs/Classes/Builder.swift b/RIBs/Classes/Builder.swift index 51f2300..d87c02f 100644 --- a/RIBs/Classes/Builder.swift +++ b/RIBs/Classes/Builder.swift @@ -20,7 +20,7 @@ import Foundation nonisolated public protocol Buildable: AnyObject {} /// Utility that instantiates a RIB and sets up its internal wirings. -open class Builder: Buildable { +nonisolated open class Builder: Buildable { /// The dependency used for this builder to build the RIB. public let dependency: DependencyType diff --git a/RIBs/Classes/DI/Component.swift b/RIBs/Classes/DI/Component.swift index 4a178ff..4a3663c 100644 --- a/RIBs/Classes/DI/Component.swift +++ b/RIBs/Classes/DI/Component.swift @@ -23,7 +23,7 @@ import Foundation /// /// A component subclass implementation should conform to child 'Dependency' protocols, defined by all of its immediate /// children. -open class Component: Dependency { +nonisolated open class Component: Dependency { /// The dependency of this `Component`. public let dependency: DependencyType From 185b3a4c24d4a68b381a3658255da0204e52175c Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 17:16:47 -0500 Subject: [PATCH 28/33] Put back the leak detector code --- RIBs/Classes/LeakDetector/LeakDetector.swift | 39 ++++++++++---------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/RIBs/Classes/LeakDetector/LeakDetector.swift b/RIBs/Classes/LeakDetector/LeakDetector.swift index aae44e2..0495daa 100644 --- a/RIBs/Classes/LeakDetector/LeakDetector.swift +++ b/RIBs/Classes/LeakDetector/LeakDetector.swift @@ -83,6 +83,7 @@ public class LeakDetector { public func expectDeallocate(object: AnyObject, inTime time: TimeInterval = LeakDefaultExpectationTime.deallocation) -> LeakDetectionHandle { expectationCount.accept(expectationCount.value + 1) + let objectDescription = String(describing: object) let objectId = String(ObjectIdentifier(object).hashValue) as NSString trackingObjects.setObject(object, forKey: objectId) @@ -90,25 +91,25 @@ public class LeakDetector { self.expectationCount.accept(self.expectationCount.value - 1) } -// Executor.execute(withDelay: time) { -// // Retain the handle so we can check for the cancelled status. Also cannot use the cancellable -// // concurrency API since the returned handle must be retained to ensure closure is executed. -// if !handle.cancelled { -// let didDeallocate = (self.trackingObjects.object(forKey: objectId) == nil) -// let message = "<\(objectDescription): \(objectId)> has leaked. Objects are expected to be deallocated at this time: \(self.trackingObjects)" -// -// if self.disableLeakDetector { -// if !didDeallocate { -// print("Leak detection is disabled. This should only be used for debugging purposes.") -// print(message) -// } -// } else { -// assert(didDeallocate, message) -// } -// } -// -// self.expectationCount.accept(self.expectationCount.value - 1) -// } + Executor.execute(withDelay: time) { + // Retain the handle so we can check for the cancelled status. Also cannot use the cancellable + // concurrency API since the returned handle must be retained to ensure closure is executed. + if !handle.cancelled { + let didDeallocate = (self.trackingObjects.object(forKey: objectId) == nil) + let message = "<\(objectDescription): \(objectId)> has leaked. Objects are expected to be deallocated at this time: \(self.trackingObjects)" + + if self.disableLeakDetector { + if !didDeallocate { + print("Leak detection is disabled. This should only be used for debugging purposes.") + print(message) + } + } else { + assert(didDeallocate, message) + } + } + + self.expectationCount.accept(self.expectationCount.value - 1) + } return handle } From b03fa92f7e7f356b0ff262a54e8b23541241ebc9 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 19:23:32 -0500 Subject: [PATCH 29/33] Fix tests for new concurrency --- RIBs/Classes/PresentableInteractor.swift | 2 +- RIBs/Classes/ViewableRouter.swift | 5 ++- RIBsTests/LeakDetector/LeakDetectorMock.swift | 44 ++++++++++++++----- RIBsTests/Router/ViewableRouterTests.swift | 35 ++++++++++++--- 4 files changed, 66 insertions(+), 20 deletions(-) diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index 3bca352..81b02fe 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -31,7 +31,7 @@ nonisolated open class PresentableInteractor: Interactor { self.presenter = presenter } - public nonisolated func presentOnMainThread(_ block: @escaping @MainActor (PresenterType) -> Void) { + public nonisolated func presentOnMainThread(_ block: @escaping @MainActor (_ presenter: PresenterType) -> Void) { nonisolated(unsafe) let presenter = self.presenter Task { @MainActor in diff --git a/RIBs/Classes/ViewableRouter.swift b/RIBs/Classes/ViewableRouter.swift index 8645aa4..1d9a3d5 100644 --- a/RIBs/Classes/ViewableRouter.swift +++ b/RIBs/Classes/ViewableRouter.swift @@ -94,9 +94,10 @@ open class ViewableRouter: Router { + // Note: The get block for a computed property is synchronous. + queue.sync { + self._statusCallCount += 1 + } + return super.status + } + + + var onDeallocateCalled: (() -> Void)? + private var _expectDeallocateCallCount = 0 + var expectDeallocateCallCount: Int { + queue.sync { self._expectDeallocateCallCount } + } override func expectDeallocate(object: AnyObject, inTime time: TimeInterval) -> LeakDetectionHandle { - expectDeallocateCallCount += 1 + queue.sync { + self._expectDeallocateCallCount += 1 + } + onDeallocateCalled?() return LeakDetectionHandleMock() } - var expectViewControllerDisappearCallCount = 0 + + var onViewControllerDisappearCalled: (() -> Void)? + private var _expectViewControllerDisappearCallCount = 0 + var expectViewControllerDisappearCallCount: Int { + queue.sync { self._expectViewControllerDisappearCallCount } + } override func expectViewControllerDisappear(viewController: UIViewController, inTime time: TimeInterval) -> LeakDetectionHandle { - expectViewControllerDisappearCallCount += 1 + queue.sync { + self._expectViewControllerDisappearCallCount += 1 + } + onViewControllerDisappearCalled?() return LeakDetectionHandleMock() } - - var statusCallCount = 0 - override var status: Observable { - statusCallCount += 1 - return super.status - } } diff --git a/RIBsTests/Router/ViewableRouterTests.swift b/RIBsTests/Router/ViewableRouterTests.swift index 049f8e7..6b5caeb 100644 --- a/RIBsTests/Router/ViewableRouterTests.swift +++ b/RIBsTests/Router/ViewableRouterTests.swift @@ -31,27 +31,48 @@ final class ViewableRouterTests: XCTestCase { LeakDetector.setInstance(leakDetectorMock) } - func test_leakDetection() async { + func test_leakDetection() { // given let interactor = PresentableInteractor(presenter: PresenterMock()) - let viewController = await ViewControllerMock() + let viewController = ViewControllerMock() router = ViewableRouter(interactor: interactor, viewController: viewController) router.load() + + let disappearExpectation = self.expectation(description: "Wait for view controller to disappear") + + leakDetectorMock.onViewControllerDisappearCalled = { [weak leakDetectorMock] in + if leakDetectorMock?.expectViewControllerDisappearCallCount == 1 { + disappearExpectation.fulfill() + } + } + // when interactor.deactivate() + // then - XCTAssertEqual(leakDetectorMock.expectViewControllerDisappearCallCount, 1) + wait(for: [disappearExpectation], timeout: 2.0) } - - func test_deinit_triggers_leakDetection() async { + + func test_deinit_triggers_leakDetection() { // given let interactor = PresentableInteractor(presenter: PresenterMock()) - let viewController = await ViewControllerMock() + let viewController = ViewControllerMock() router = ViewableRouter(interactor: interactor, viewController: viewController) router.load() + // when + let deallocationExpectation = self.expectation(description: "Expect deallocate to be called twice") + + leakDetectorMock.onDeallocateCalled = { [weak leakDetectorMock] in + + if leakDetectorMock?.expectDeallocateCallCount == 2 { + deallocationExpectation.fulfill() + } + } + // when router = nil + // then - XCTAssertEqual(leakDetectorMock.expectDeallocateCallCount, 2) + wait(for: [deallocationExpectation], timeout: 5.0) } } From c2e8007f2d6c0a818441cd32c2e59bcc6208091e Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 11 Oct 2025 20:05:58 -0500 Subject: [PATCH 30/33] Make presenatble sendable --- RIBs/Classes/Presenter.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RIBs/Classes/Presenter.swift b/RIBs/Classes/Presenter.swift index 6944c07..ce3ef9d 100644 --- a/RIBs/Classes/Presenter.swift +++ b/RIBs/Classes/Presenter.swift @@ -18,7 +18,7 @@ import Foundation /// The base protocol for all `Presenter`s. @MainActor -public protocol Presentable: AnyObject { +public protocol Presentable: AnyObject, Sendable { associatedtype Listener nonisolated var listener: Listener? { get set } @@ -27,7 +27,7 @@ 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. -open class Presenter: @MainActor Presentable { +open class Presenter: Presentable { nonisolated(unsafe) public var listener: Listener? public typealias Listener = Any From 884e26d6a448870870b9ba09a9d17fa701ba407c Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 18 Oct 2025 13:28:39 -0500 Subject: [PATCH 31/33] Make presentable interactor an unchecked sendable --- RIBs/Classes/PresentableInteractor.swift | 2 +- RIBs/Classes/Presenter.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index 81b02fe..4429d16 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -17,7 +17,7 @@ import Foundation /// Base class of an `Interactor` that actually has an associated `Presenter` and `View`. -nonisolated open class PresentableInteractor: Interactor { +nonisolated open class PresentableInteractor: Interactor { /// The `Presenter` associated with this `Interactor`. public let presenter: PresenterType diff --git a/RIBs/Classes/Presenter.swift b/RIBs/Classes/Presenter.swift index ce3ef9d..27b8653 100644 --- a/RIBs/Classes/Presenter.swift +++ b/RIBs/Classes/Presenter.swift @@ -18,7 +18,7 @@ import Foundation /// The base protocol for all `Presenter`s. @MainActor -public protocol Presentable: AnyObject, Sendable { +public protocol Presentable: AnyObject & SendableMetatype { associatedtype Listener nonisolated var listener: Listener? { get set } @@ -28,9 +28,9 @@ public protocol Presentable: AnyObject, Sendable { /// `ViewController` can consume and display. It also maps UI events to business logic method, invoked to /// its listener. open class Presenter: Presentable { - nonisolated(unsafe) public var listener: Listener? + nonisolated(unsafe) public weak var listener: Listener? - public typealias Listener = Any + public typealias Listener = AnyObject /// The view controller of this presenter. From 9c00dd7ddf5833a1d71e808301009110777da439 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 18 Oct 2025 17:08:27 -0500 Subject: [PATCH 32/33] Make interacor @unchecked Sendable - add helper method for UI navigation to ViewableRouter - add docs to presenter helper method and to the viewable router helper methods --- RIBs/Classes/Interactor.swift | 2 +- RIBs/Classes/PresentableInteractor.swift | 42 +++++++++++++ RIBs/Classes/ViewableRouter.swift | 75 ++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/RIBs/Classes/Interactor.swift b/RIBs/Classes/Interactor.swift index f06add0..635a365 100644 --- a/RIBs/Classes/Interactor.swift +++ b/RIBs/Classes/Interactor.swift @@ -63,7 +63,7 @@ nonisolated public protocol Interactable: InteractorScope { /// active. /// /// An `Interactor` should only perform its business logic when it's currently active. -nonisolated open class Interactor: Interactable { +nonisolated open class Interactor: Interactable, @unchecked Sendable { /// Indicates if the interactor is active. diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index 4429d16..d0931ac 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -31,6 +31,48 @@ nonisolated open class PresentableInteractor Void) { nonisolated(unsafe) let presenter = self.presenter diff --git a/RIBs/Classes/ViewableRouter.swift b/RIBs/Classes/ViewableRouter.swift index 1d9a3d5..2e998fb 100644 --- a/RIBs/Classes/ViewableRouter.swift +++ b/RIBs/Classes/ViewableRouter.swift @@ -54,7 +54,82 @@ open class ViewableRouter: Router Void) { + nonisolated(unsafe) let thisRouterViewController = self.viewController + Task { @MainActor in + block(thisRouterViewController) + } + } + + /// A helper method to safely call viewController methods for UI navigation on the main thread with a child view controller. + /// This overload provides both the current router's view controller and a child view controller to the closure, + /// ensuring all UI operations happen on the main thread. + /// + /// When routing in RIBs that have UI (using ViewableRouter), the plumbing of routing such as clearing out + /// child RIB references, setting them up, or attaching/detaching child routers can happen on any thread. + /// However, the actual physical/mechanical navigation of the UI - such as pushing or popping child RIB's UI + /// onto/off the navigation controller stack, modally presenting or dismissing it, or attaching/detaching it + /// from the UI hierarchy via custom implementation using child containment API - must all be done on the main thread + /// since they manipulate the UI tree and trigger UI rendering. + /// + /// This method ensures that your viewController method calls run on the main thread. + /// Use this method when you encounter compiler warnings such as: + /// "Main actor-isolated property 'uiviewController' cannot be referenced from a nonisolated context" + /// + /// This is an overload of the basic `navigateOnMainThread(_:)` method that includes a child view controller parameter. + /// Use this method when you need to perform UI navigation that involves both the current router's view controller + /// and a child view controller, such as presenting, pushing, or custom containment operations. + /// For navigation operations that only involve the current router's view controller, use the basic `navigateOnMainThread(_:)` method instead. + /// + /// Example usage - routing to child RIB: + /// ```swift + /// func routeToChildRIB() { + /// let childRIBRouter = childRIBBuilder.build(withListener: interactor) + /// self.childRIBRouter = childRIBRouter + /// navigateOnMainThread(with: childRIBRouter.viewControllable) { thisRouterViewController, childViewController in + /// thisRouterViewController.attachChildRIBViewController(childViewController.uiviewController) + /// } + /// attachChild(childRIBRouter) + /// } + /// ``` + + public nonisolated func navigateOnMainThread(with childViewController: ViewControllable, _ block: @escaping @MainActor (_ thisRouterViewController: ViewControllerType, _ childViewController: ViewControllable) -> Void) { + nonisolated(unsafe) let thisRouterViewController = self.viewController + nonisolated(unsafe) let childVC = childViewController + + Task { @MainActor in + block(thisRouterViewController, childVC) + } + } + // MARK: - Internal override func internalDidLoad() { From acbaea93a8c76be2efc2d2e1f0ce3ac83951c231 Mon Sep 17 00:00:00 2001 From: Alex Bush Date: Sat, 18 Oct 2025 17:34:51 -0500 Subject: [PATCH 33/33] Fix tests --- Package.swift | 3 +-- RIBs/Classes/PresentableInteractor.swift | 2 +- .../Interactor/PresentableInteractorTests.swift | 14 ++++++++++---- RIBsTests/Mocks.swift | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Package.swift b/Package.swift index 60f6343..8508f69 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( name: "RIBs", platforms: [ - .iOS(.v18) + .iOS(.v15) ], products: [ .library(name: "RIBs", targets: ["RIBs"]), @@ -22,7 +22,6 @@ let package = Package( ], path: "RIBs", swiftSettings: [ -// .unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"]), .swiftLanguageMode(.v6), .enableUpcomingFeature("StrictConcurrency"), ] diff --git a/RIBs/Classes/PresentableInteractor.swift b/RIBs/Classes/PresentableInteractor.swift index d0931ac..7840354 100644 --- a/RIBs/Classes/PresentableInteractor.swift +++ b/RIBs/Classes/PresentableInteractor.swift @@ -17,7 +17,7 @@ import Foundation /// Base class of an `Interactor` that actually has an associated `Presenter` and `View`. -nonisolated open class PresentableInteractor: Interactor { +nonisolated open class PresentableInteractor: Interactor, @unchecked Sendable { /// The `Presenter` associated with this `Interactor`. public let presenter: PresenterType diff --git a/RIBsTests/Interactor/PresentableInteractorTests.swift b/RIBsTests/Interactor/PresentableInteractorTests.swift index 7505703..30175a0 100644 --- a/RIBsTests/Interactor/PresentableInteractorTests.swift +++ b/RIBsTests/Interactor/PresentableInteractorTests.swift @@ -9,13 +9,19 @@ import XCTest import RxSwift -protocol TestPresenter {} +protocol MockPresentableListener: AnyObject { + +} -final class PresenterMock: TestPresenter {} +protocol TestPresenter: Presentable where Listener == MockPresentableListener {} + +final class PresenterMock: TestPresenter { + nonisolated(unsafe) weak var listener: MockPresentableListener? +} final class PresentableInteractorTests: XCTestCase { - private var interactor: PresentableInteractor! + private var interactor: PresentableInteractor! override func setUp() { super.setUp() @@ -26,7 +32,7 @@ final class PresentableInteractorTests: XCTestCase { // given let presenterMock = PresenterMock() let disposeBag = DisposeBag() - interactor = PresentableInteractor(presenter: presenterMock) + interactor = PresentableInteractor(presenter: presenterMock) var status: LeakDetectionStatus = .DidComplete LeakDetector.instance.status.subscribe { newStatus in status = newStatus diff --git a/RIBsTests/Mocks.swift b/RIBsTests/Mocks.swift index a95ea62..4f06dea 100644 --- a/RIBsTests/Mocks.swift +++ b/RIBsTests/Mocks.swift @@ -45,7 +45,7 @@ class ViewControllableMock: ViewControllable { let uiviewController = UIViewController(nibName: nil, bundle: nil) } -class InteractorMock: Interactor { +class InteractorMock: Interactor, @unchecked Sendable { var didBecomeActiveHandler: (() -> ())? var didBecomeActiveCallCount: Int = 0 var willResignActiveHandler: (() -> ())?