From ad6ec0fc6fd627b120111aa717793cfde25e2778 Mon Sep 17 00:00:00 2001 From: Daniel Hok Date: Mon, 23 Sep 2024 16:19:27 -0400 Subject: [PATCH] Version 11.0.0 --- BrazeKit.podspec | 6 +- BrazeKitCompat.podspec | 8 +- BrazeLocation.podspec | 8 +- BrazeNotificationService.podspec | 6 +- BrazePushStory.podspec | 6 +- BrazeUI.podspec | 6 +- BrazeUICompat.podspec | 8 +- CHANGELOG.md | 20 +++++ .../Examples-Manual.xcodeproj/project.pbxproj | 2 + .../project.pbxproj | 10 ++- .../ObjC/Sources/PushNotifications/Info.plist | 30 ++++++++ .../PushNotifications.entitlements | 12 +++ Examples/ObjC/manual-integration-setup.sh | 2 +- .../project.pbxproj | 6 ++ .../Examples-Manual.xcodeproj/project.pbxproj | 6 ++ .../project.pbxproj | 14 +++- .../Analytics/AuthenticationManager.swift | 1 + Examples/Swift/Sources/Analytics/Readme.swift | 8 +- .../Sources/Common/ReadmeViewController.swift | 6 +- .../AppDelegate.swift | 2 + .../ContentCardUI-Customization/Readme.swift | 52 ++++++++----- .../Swift/Sources/ContentCardUI/Readme.swift | 6 +- .../ContentCards-Custom-UI/Readme.swift | 9 ++- .../InAppMessageUI-Customization/Readme.swift | 26 ++++++- .../Swift/Sources/InAppMessageUI/Readme.swift | 5 +- .../InAppMessageInfoViewController.swift | 1 + .../InAppMessages-Custom-UI/Readme.swift | 5 +- Examples/Swift/Sources/Location/Readme.swift | 9 ++- .../PushNotifications-Automatic/Readme.swift | 3 +- .../Readme.swift | 4 +- .../AppDelegate+Xcode16.swift | 47 ++++++++++++ .../AppDelegate.swift | 75 ++++++++++--------- .../PushNotifications-Manual/Readme.swift | 3 +- .../Sources/PushNotifications/Info.plist | 30 ++++++++ .../PushNotifications.entitlements | 12 +++ Examples/Swift/manual-integration-setup.sh | 2 +- Package.swift | 18 ++--- README.md | 2 +- .../Cells/ContentCardUICell.swift | 4 +- .../BrazeUI/ContentCardUI/ContentCardUI.swift | 1 + .../ContentCardUIModalViewController.swift | 8 +- .../ContentCardUI/ContentCardUISwiftUI.swift | 12 +-- .../ContentCardUIViewController.swift | 24 ++++-- ...entCardUIViewControllerDelegate+ObjC.swift | 10 ++- .../ContentCardUIViewControllerDelegate.swift | 1 + Sources/BrazeUI/Dependencies/Align.swift | 45 +++++++++++ .../Dependencies/ConcurrencyHelper.swift | 32 ++++++++ .../Dependencies/GIFViewProvider+ObjC.swift | 13 ++-- .../Dependencies/GIFViewProvider.swift | 23 ++++-- .../Dependencies/KeyboardFrameNotifier.swift | 1 + Sources/BrazeUI/Dependencies/Shadow.swift | 2 +- .../BrazeUI/Dependencies/ViewDimension.swift | 2 +- .../Dependencies/VisibilityTracker.swift | 13 ++-- .../InAppMessageUI/InAppMessageUI.swift | 13 +++- .../InAppMessageUIDelegate+ObjC.swift | 18 ++++- .../InAppMessageUIDelegate.swift | 7 ++ .../InAppMessageUI/InAppMessageUIExt.swift | 4 +- .../InAppMessageUIPresentationContext.swift | 1 + ...InAppMessageUIPresentationContextRaw.swift | 3 +- .../InAppMessageUIViewController.swift | 3 +- .../InAppMessageViewAttributes.swift | 2 +- .../Views/InAppMessageUIContainerView.swift | 10 ++- .../Views/InAppMessageUIFullImageView.swift | 25 +++++-- .../Views/InAppMessageUIFullView.swift | 25 +++++-- .../Views/InAppMessageUIHtmlView.swift | 39 +++++++--- .../Views/InAppMessageUIModalImageView.swift | 23 ++++-- .../Views/InAppMessageUIModalView.swift | 23 ++++-- .../Views/InAppMessageUISlideupView.swift | 28 +++++-- .../Views/InAppMessageView.swift | 1 + .../Views/Misc/ButtonView.swift | 17 ++++- .../InAppMessageUI/Views/Misc/IconView.swift | 2 +- .../Views/Misc/ModalTextView.swift | 6 +- .../Views/Misc/StackView+ButtonViews.swift | 2 +- .../InAppMessageUI/Views/Misc/StackView.swift | 4 +- Sources/BrazeUI/Resources.swift | 16 +++- 75 files changed, 726 insertions(+), 213 deletions(-) create mode 100644 Examples/ObjC/Sources/PushNotifications/Info.plist create mode 100644 Examples/ObjC/Sources/PushNotifications/PushNotifications.entitlements create mode 100644 Examples/Swift/Sources/PushNotifications-Manual/AppDelegate+Xcode16.swift create mode 100644 Examples/Swift/Sources/PushNotifications/Info.plist create mode 100644 Examples/Swift/Sources/PushNotifications/PushNotifications.entitlements create mode 100644 Sources/BrazeUI/Dependencies/ConcurrencyHelper.swift diff --git a/BrazeKit.podspec b/BrazeKit.podspec index e463144c8c..ca8f52ed09 100644 --- a/BrazeKit.podspec +++ b/BrazeKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeKit' - s.version = '10.3.1' + s.version = '11.0.0' s.summary = 'Braze Main SDK library providing support for analytics and push notifications.' s.homepage = 'https://braze.com' @@ -9,8 +9,8 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/10.3.1/BrazeKit.zip', - :sha256 => '5bba5938771c81c6bbc1909098f749487c901f950ab16c84d01bcf38e2b8fe82' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/11.0.0/BrazeKit.zip', + :sha256 => '0a236bea81a47f3a3835d09c1e631d45039bd4baefca1a791b3c0085a415145f' } s.swift_version = '5.0' diff --git a/BrazeKitCompat.podspec b/BrazeKitCompat.podspec index d913ad8863..203cd54e09 100644 --- a/BrazeKitCompat.podspec +++ b/BrazeKitCompat.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeKitCompat' - s.version = '10.3.1' + s.version = '11.0.0' s.summary = 'Compatibility library for users migrating from AppboyKit.' s.homepage = 'https://braze.com' @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.license = { :type => 'Commercial' } s.authors = 'Braze, Inc.' - s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '10.3.1' } + s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '11.0.0' } s.swift_version = '5.0' s.ios.deployment_target = '12.0' @@ -18,8 +18,8 @@ Pod::Spec.new do |s| s.public_header_files = 'Sources/BrazeKitCompat/include/*.h' s.static_framework = true - s.dependency 'BrazeKit', '10.3.1' - s.dependency 'BrazeLocation', '10.3.1' + s.dependency 'BrazeKit', '11.0.0' + s.dependency 'BrazeLocation', '11.0.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/BrazeLocation.podspec b/BrazeLocation.podspec index d3f2587a5d..03f5ba97c3 100644 --- a/BrazeLocation.podspec +++ b/BrazeLocation.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeLocation' - s.version = '10.3.1' + s.version = '11.0.0' s.summary = 'Braze location library providing support for location analytics and geofence monitoring.' s.homepage = 'https://braze.com' @@ -9,8 +9,8 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/10.3.1/BrazeLocation.zip', - :sha256 => 'cd85ecb7157f7133461794d6609ef2c052ed983d038c75a78f1cf0c120f819fd' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/11.0.0/BrazeLocation.zip', + :sha256 => '4962245ad3fc46efe1ae4d08926ce7cedc6bb54db42fbc37824a4f316cd3bfbe' } s.swift_version = '5.0' @@ -21,7 +21,7 @@ Pod::Spec.new do |s| s.vendored_framework = 'BrazeLocation.xcframework' s.resource_bundles = { 'BrazeLocation' => ['Sources/BrazeLocationResources/Resources/**/*'] } - s.dependency 'BrazeKit', '10.3.1' + s.dependency 'BrazeKit', '11.0.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/BrazeNotificationService.podspec b/BrazeNotificationService.podspec index 71a583dd39..514e7db5a5 100644 --- a/BrazeNotificationService.podspec +++ b/BrazeNotificationService.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeNotificationService' - s.version = '10.3.1' + s.version = '11.0.0' s.summary = 'Braze notification service extension library providing support for Rich Push notifications.' s.homepage = 'https://braze.com' @@ -9,8 +9,8 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/10.3.1/BrazeNotificationService.zip', - :sha256 => '178183799f3b4f6b2f4b484152ec2cf577e761e7b43da3002a754a179ba87290' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/11.0.0/BrazeNotificationService.zip', + :sha256 => 'dfbcbdb4e37789a229743be1009d9f00811cd51b80d6c0d00f4ded2e75c85d76' } s.swift_version = '5.0' diff --git a/BrazePushStory.podspec b/BrazePushStory.podspec index ea6eadcbf7..87ddf8b1a0 100644 --- a/BrazePushStory.podspec +++ b/BrazePushStory.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazePushStory' - s.version = '10.3.1' + s.version = '11.0.0' s.summary = 'Braze notification content extension library providing support for Push Stories.' s.homepage = 'https://braze.com' @@ -9,8 +9,8 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/10.3.1/BrazePushStory.zip', - :sha256 => 'bf807697dbe65acb25ae780d2815a39b32742592998f80f5120e3af7ff1b8132' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/11.0.0/BrazePushStory.zip', + :sha256 => '8e3579612aed0d7c16092683bd4fe99fe3ba6e8aa8a1bec103dd001b5e0891d5' } s.swift_version = '5.0' diff --git a/BrazeUI.podspec b/BrazeUI.podspec index d66475ee68..fe7da9c62a 100644 --- a/BrazeUI.podspec +++ b/BrazeUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeUI' - s.version = '10.3.1' + s.version = '11.0.0' s.summary = 'Braze-provided user interface library for In-App Messages and Content Cards.' s.homepage = 'https://braze.com' @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.license = { :type => 'Commercial' } s.authors = 'Braze, Inc.' - s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '10.3.1' } + s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '11.0.0' } s.swift_version = '5.0' s.ios.deployment_target = '12.0' @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.resource_bundles = { 'BrazeUI' => ['Sources/BrazeUI/Resources/**/*'] } s.static_framework = true - s.dependency 'BrazeKit', '10.3.1' + s.dependency 'BrazeKit', '11.0.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/BrazeUICompat.podspec b/BrazeUICompat.podspec index d56783e065..1e63ed57cf 100644 --- a/BrazeUICompat.podspec +++ b/BrazeUICompat.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeUICompat' - s.version = '10.3.1' + s.version = '11.0.0' s.summary = 'Compatibility UI library for users migrating from AppboyUI.' s.homepage = 'https://braze.com' @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.license = { :type => 'Commercial' } s.authors = 'Braze, Inc.' - s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '10.3.1' } + s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '11.0.0' } s.swift_version = '5.0' s.ios.deployment_target = '12.0' @@ -18,8 +18,8 @@ Pod::Spec.new do |s| s.resource_bundles = { 'BrazeUICompat' => 'Sources/BrazeUICompat/*/Resources/**/*.*' } s.static_framework = true - s.dependency 'BrazeKitCompat', '10.3.1' - s.dependency 'SDWebImage', '>= 5.19.0', '< 6' + s.dependency 'BrazeKitCompat', '11.0.0' + s.dependency 'SDWebImage', '>= 5.19.7', '< 6' s.user_target_xcconfig = { 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES' } s.pod_target_xcconfig = { diff --git a/CHANGELOG.md b/CHANGELOG.md index 58223ffcac..24fc9dde72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## 11.0.0 + +##### Breaking +- Adds support for [Swift 6 strict concurrency checking](https://developer.apple.com/documentation/swift/adoptingswift6). + - Relevant public Braze classes and data types now conform to the `Sendable` protocol and can be safely used across concurrency contexts. + - Main thread-only APIs are now marked with the `@MainActor` attribute. + - We recommend using Xcode 16.0 or later to take advantage of these features while minimizing the number of warnings generated by the compiler. Previous versions of Xcode may still be used, but some features may generate warnings. +- When integrating push notification support manually, you may need to update the `UNUserNotificationCenterDelegate` conformance to use the `@preconcurrency` attribute to prevent warnings. + - Applying the `@preconcurrency` attribute on protocol conformance is only available in Xcode 16.0 or later. Reference our sample integration code [here](https://github.com/braze-inc/braze-swift-sdk/tree/main/Examples/Swift/Sources/PushNotifications-Manual). + - As of Xcode 16.0, Apple has not yet audited the `UNUserNotificationCenterDelegate` protocol for Swift concurrency. + ```swift + extension AppDelegate: @preconcurrency UNUserNotificationCenterDelegate { + // Your existing implementation + } + ``` +- Updates the `SDWebImage` dependency in `BrazeUICompat` and sample apps to `5.19.7+` to support Swift 6 strict concurrency checking. + +#### Fixed +- Fixes the push authorization status reporting to display the proper push token status on the Dashboard when a user has not explicitly accepted or declined push permissions. + ## 10.3.1 ##### Fixed diff --git a/Examples/ObjC/Examples-Manual.xcodeproj/project.pbxproj b/Examples/ObjC/Examples-Manual.xcodeproj/project.pbxproj index 40371efed2..eccd126d3a 100644 --- a/Examples/ObjC/Examples-Manual.xcodeproj/project.pbxproj +++ b/Examples/ObjC/Examples-Manual.xcodeproj/project.pbxproj @@ -1904,6 +1904,7 @@ SDKROOT = auto; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; XROS_DEPLOYMENT_TARGET = 1.0; @@ -1962,6 +1963,7 @@ SDKROOT = auto; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; XROS_DEPLOYMENT_TARGET = 1.0; diff --git a/Examples/ObjC/Examples-SwiftPM.xcodeproj/project.pbxproj b/Examples/ObjC/Examples-SwiftPM.xcodeproj/project.pbxproj index f822bfb06f..4c065d21f4 100644 --- a/Examples/ObjC/Examples-SwiftPM.xcodeproj/project.pbxproj +++ b/Examples/ObjC/Examples-SwiftPM.xcodeproj/project.pbxproj @@ -963,7 +963,7 @@ mainGroup = D8CC2266D736859D7DD9A8FF; packageReferences = ( 9928D5150C45879A982BA1C6 /* XCRemoteSwiftPackageReference "SDWebImage" */, - 90D8318C2DB3CED6610DD38C /* XCLocalSwiftPackageReference "/Users/jerielng/swift-sdk/.build/braze/release/braze-swift-sdk" */, + B0CAEC5E933F542260FE1612 /* XCLocalSwiftPackageReference "/Users/daniel.hok/Desktop/swift-sdk/.build/braze/release/braze-swift-sdk" */, ); projectDirPath = ""; projectRoot = ""; @@ -1427,6 +1427,7 @@ SDKROOT = auto; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; XROS_DEPLOYMENT_TARGET = 1.0; @@ -1552,6 +1553,7 @@ SDKROOT = auto; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; XROS_DEPLOYMENT_TARGET = 1.0; @@ -2115,15 +2117,15 @@ repositoryURL = "https://github.com/SDWebImage/SDWebImage"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.19.0; + minimumVersion = 5.19.7; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */ - 90D8318C2DB3CED6610DD38C /* XCLocalSwiftPackageReference "/Users/jerielng/swift-sdk/.build/braze/release/braze-swift-sdk" */ = { + B0CAEC5E933F542260FE1612 /* XCLocalSwiftPackageReference "/Users/daniel.hok/Desktop/swift-sdk/.build/braze/release/braze-swift-sdk" */ = { isa = XCLocalSwiftPackageReference; - relativePath = "/Users/jerielng/swift-sdk/.build/braze/release/braze-swift-sdk"; + relativePath = "/Users/daniel.hok/Desktop/swift-sdk/.build/braze/release/braze-swift-sdk"; }; /* End XCLocalSwiftPackageReference section */ diff --git a/Examples/ObjC/Sources/PushNotifications/Info.plist b/Examples/ObjC/Sources/PushNotifications/Info.plist new file mode 100644 index 0000000000..d7f8ec8ca2 --- /dev/null +++ b/Examples/ObjC/Sources/PushNotifications/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + PushNotifications + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + LaunchScreen + + diff --git a/Examples/ObjC/Sources/PushNotifications/PushNotifications.entitlements b/Examples/ObjC/Sources/PushNotifications/PushNotifications.entitlements new file mode 100644 index 0000000000..9cad402b51 --- /dev/null +++ b/Examples/ObjC/Sources/PushNotifications/PushNotifications.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.security.application-groups + + group.com.braze.PushNotifications.PushStories + + + diff --git a/Examples/ObjC/manual-integration-setup.sh b/Examples/ObjC/manual-integration-setup.sh index 8292ceab62..7f388cb6bd 100755 --- a/Examples/ObjC/manual-integration-setup.sh +++ b/Examples/ObjC/manual-integration-setup.sh @@ -20,7 +20,7 @@ if [ ! -f "manual-integration-setup.sh" ]; then fi # Constants -url="https://github.com/braze-inc/braze-swift-sdk/releases/download/10.3.1/braze-swift-sdk-prebuilt.zip" +url="https://github.com/braze-inc/braze-swift-sdk/releases/download/11.0.0/braze-swift-sdk-prebuilt.zip" echo "→" "Cleaning up" rm -rf braze-swift-sdk-prebuilt diff --git a/Examples/Swift/Examples-CocoaPods.xcodeproj/project.pbxproj b/Examples/Swift/Examples-CocoaPods.xcodeproj/project.pbxproj index c85ee3bfc4..55dce4b5a0 100644 --- a/Examples/Swift/Examples-CocoaPods.xcodeproj/project.pbxproj +++ b/Examples/Swift/Examples-CocoaPods.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 345CA2282BFEFED98328C0C2 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6992ACA4C973FAF6333EE4B2 /* Readme.swift */; }; 34C9C566ACB7B6220C782866 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5A56AB422F8DA782A8D2B6FD /* LaunchScreen.storyboard */; }; 356FD93DCAEAD68D24E91795 /* InAppMessageInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D93378C85712C1E5E9146B7 /* InAppMessageInfoViewController.swift */; }; + 3781F2BE6289AA47C84F66EB /* AppDelegate+Xcode16.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52769C8F4926DE1AE9B5C40 /* AppDelegate+Xcode16.swift */; }; 383D757BFC1CFD0BE5D70CAB /* MockImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1099A7C7E5E89E130751B24C /* MockImage.swift */; }; 3848E121BD7880E965B1E928 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6742114C06FC93D4AA30B5B /* Readme.swift */; }; 3C6B1FC5F61A52BDC162A342 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0340E1FFDDBC0C9D72FCD4D4 /* Assets.xcassets */; }; @@ -96,6 +97,7 @@ 67058DC19EFF4F37D190035A /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0078638D97BB480D82B18D6C /* Credentials.swift */; }; 684401812C4C2490603788C1 /* FullWidthSlideupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C74A8AB0317373366057E4 /* FullWidthSlideupView.swift */; }; 6A1B61950E0EEE8F2CC58222 /* MockImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1099A7C7E5E89E130751B24C /* MockImage.swift */; }; + 6B11762CE968AA3C64156187 /* AppDelegate+Xcode16.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52769C8F4926DE1AE9B5C40 /* AppDelegate+Xcode16.swift */; }; 6BBEF5F38216AE455D0DD78F /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B8597B8B1BE2497CEFD038 /* Readme.swift */; }; 6CE1D3089C764A51A5747648 /* SDWebImageGIFViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20ACDCAF82FAFB8B4915FF5D /* SDWebImageGIFViewProvider.swift */; }; 6D0FFAC9BD9D9E6A49827977 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBBD41BB997EBF5820D819F /* AppDelegate.swift */; }; @@ -462,6 +464,7 @@ D6742114C06FC93D4AA30B5B /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; D6D87B0B28882DFD35A2BE4B /* InAppMessageUI-Customization.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "InAppMessageUI-Customization.app"; sourceTree = BUILT_PRODUCTS_DIR; }; DE3608B9E460175A349CDB87 /* Analytics.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Analytics.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E52769C8F4926DE1AE9B5C40 /* AppDelegate+Xcode16.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Xcode16.swift"; sourceTree = ""; }; EB96485E099438DAB9547DF5 /* PushNotificationsContentExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = PushNotificationsContentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; EC4AEEA68D47995F66CD4034 /* ContentCardUI.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = ContentCardUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; F224F57B3C4BE13C161148B0 /* SDWebImageGIFViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageGIFViewProvider.swift; sourceTree = ""; }; @@ -536,6 +539,7 @@ isa = PBXGroup; children = ( 0BA4A3B91B682BDF5707A9B8 /* AppDelegate.swift */, + E52769C8F4926DE1AE9B5C40 /* AppDelegate+Xcode16.swift */, 56301FA4B15B6867A6EC6713 /* Readme.swift */, ); name = "PushNotifications-Manual"; @@ -1898,6 +1902,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3781F2BE6289AA47C84F66EB /* AppDelegate+Xcode16.swift in Sources */, 10E6DC8D2BCBC3BEB9A57C5C /* AppDelegate.swift in Sources */, B623DADAA2CD790DE1B3FBF6 /* Credentials.swift in Sources */, 584387A3A80A86D5F630ABBE /* MockImage.swift in Sources */, @@ -1936,6 +1941,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6B11762CE968AA3C64156187 /* AppDelegate+Xcode16.swift in Sources */, D043379496CD796896B44512 /* AppDelegate.swift in Sources */, 8D3BC628AF159AB7C8F52527 /* Credentials.swift in Sources */, 139780D805B7B80896DAA823 /* MockImage.swift in Sources */, diff --git a/Examples/Swift/Examples-Manual.xcodeproj/project.pbxproj b/Examples/Swift/Examples-Manual.xcodeproj/project.pbxproj index 21050afae3..7db7745fc7 100644 --- a/Examples/Swift/Examples-Manual.xcodeproj/project.pbxproj +++ b/Examples/Swift/Examples-Manual.xcodeproj/project.pbxproj @@ -103,6 +103,7 @@ BBE19BF04F56AF61048002A8 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CEC1A3A03568BC7B5FB442 /* Credentials.swift */; }; BCDCB5A2B878D59CEC6F7A78 /* BrazeUI.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 85365B9C180AB9BA57FBD249 /* BrazeUI.bundle */; }; BF3E00BF746510504CA6D052 /* BrazeKit.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 963312B05750D1D4DA77128F /* BrazeKit.bundle */; }; + BF7233DE7007E5E231FA48BB /* AppDelegate+Xcode16.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233D96191DCA8E6AD3D42D11 /* AppDelegate+Xcode16.swift */; }; C00AE8D9608F09A56D03B49C /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E49D89F598192D2C7522BE /* Readme.swift */; }; C2C0054EA8B2CD0FE02300A7 /* BrazeLocation.bundle in Resources */ = {isa = PBXBuildFile; fileRef = EB8AF6AAFE01DAED72696307 /* BrazeLocation.bundle */; }; C52569EE1C4FC5B2E83739AC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AEF0FF04032299A31D37CDE4 /* Assets.xcassets */; }; @@ -231,6 +232,7 @@ 0617474A1F534AFBE702090C /* ReadmeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadmeViewController.swift; sourceTree = ""; }; 07DE243578DBC5FA4DF8237E /* MockImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImage.swift; sourceTree = ""; }; 1F1631DFDCE0C1B3F41E792E /* PushNotifications-Automatic.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "PushNotifications-Automatic.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 233D96191DCA8E6AD3D42D11 /* AppDelegate+Xcode16.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Xcode16.swift"; sourceTree = ""; }; 239CF1158BD0D1AB82866793 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 2FFD57A976BE8350B2E0529C /* SDWebImageGIFViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageGIFViewProvider.swift; sourceTree = ""; }; 30D72A9C07BEDB9E09FAE84D /* BrazeNotificationService.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrazeNotificationService.xcframework; path = "braze-swift-sdk-prebuilt/static/BrazeNotificationService.xcframework"; sourceTree = ""; }; @@ -614,6 +616,7 @@ isa = PBXGroup; children = ( F1CEE532EAF4BB49965CFD3D /* AppDelegate.swift */, + 233D96191DCA8E6AD3D42D11 /* AppDelegate+Xcode16.swift */, 970FEBFCFF80A684AD29EE90 /* Readme.swift */, ); name = "PushNotifications-Manual"; @@ -1214,6 +1217,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BF7233DE7007E5E231FA48BB /* AppDelegate+Xcode16.swift in Sources */, 3C0214814040DA5270457B61 /* AppDelegate.swift in Sources */, BBE19BF04F56AF61048002A8 /* Credentials.swift in Sources */, 56D4A7826E6B49E1870A71B8 /* MockImage.swift in Sources */, @@ -1892,6 +1896,7 @@ SDKROOT = auto; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; XROS_DEPLOYMENT_TARGET = 1.0; @@ -1950,6 +1955,7 @@ SDKROOT = auto; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; XROS_DEPLOYMENT_TARGET = 1.0; diff --git a/Examples/Swift/Examples-SwiftPM.xcodeproj/project.pbxproj b/Examples/Swift/Examples-SwiftPM.xcodeproj/project.pbxproj index a583ca5902..cc48652a43 100644 --- a/Examples/Swift/Examples-SwiftPM.xcodeproj/project.pbxproj +++ b/Examples/Swift/Examples-SwiftPM.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 50D8D4EB19BF8DB20DF34B8D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0FCD17EEF41713F18F4CA702 /* Assets.xcassets */; }; 52CAA705E5A2A6B084470A6B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0FCD17EEF41713F18F4CA702 /* Assets.xcassets */; }; 5489F499BAD776AAF047FE41 /* PushNotificationsContentExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A7B092ACAEC915379923328F /* PushNotificationsContentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 55575A615424AB84B62EA11D /* AppDelegate+Xcode16.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43029D0CF2FFD58212D8A976 /* AppDelegate+Xcode16.swift */; }; 56D5E2804EA244C26A6C7084 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C3766DAA0E86C999794A8F /* ReadmeViewController.swift */; }; 5C46A726B573C78D24809762 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1BF6D20F4E3962FBB5B92AF3 /* BrazeKit */; }; 61A171370197788908EC6E6C /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C3766DAA0E86C999794A8F /* ReadmeViewController.swift */; }; @@ -219,6 +220,7 @@ 2205604217E64D9BDCAB2FDF /* PushNotifications-DelayedInitialization.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "PushNotifications-DelayedInitialization.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 3779C8A71C6D78BBBD104EE0 /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; 3E837265F7BEBDDF6865917B /* PushNotifications-Manual.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "PushNotifications-Manual.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 43029D0CF2FFD58212D8A976 /* AppDelegate+Xcode16.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Xcode16.swift"; sourceTree = ""; }; 43898C005AA33A7630288625 /* ContentCardUI.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = ContentCardUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45164B1D1773E47F1B4F4671 /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; 4FE3CE0839B9CDC3DBD854F1 /* SDWebImageGIFViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageGIFViewProvider.swift; sourceTree = ""; }; @@ -422,6 +424,7 @@ isa = PBXGroup; children = ( D19D0F1AEE6BC19D74B3B363 /* AppDelegate.swift */, + 43029D0CF2FFD58212D8A976 /* AppDelegate+Xcode16.swift */, 45164B1D1773E47F1B4F4671 /* Readme.swift */, ); name = "PushNotifications-Manual"; @@ -954,7 +957,7 @@ mainGroup = D8CC2266D736859D7DD9A8FF; packageReferences = ( 9928D5150C45879A982BA1C6 /* XCRemoteSwiftPackageReference "SDWebImage" */, - 90D8318C2DB3CED6610DD38C /* XCLocalSwiftPackageReference "/Users/jerielng/swift-sdk/.build/braze/release/braze-swift-sdk" */, + B0CAEC5E933F542260FE1612 /* XCLocalSwiftPackageReference "/Users/daniel.hok/Desktop/swift-sdk/.build/braze/release/braze-swift-sdk" */, ); projectDirPath = ""; projectRoot = ""; @@ -1153,6 +1156,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 55575A615424AB84B62EA11D /* AppDelegate+Xcode16.swift in Sources */, A37DA2DB71B7AD385059771C /* AppDelegate.swift in Sources */, 0DA5B1EC47B75FAE8E3F637F /* Credentials.swift in Sources */, ABDD26A5B1458660253434E6 /* MockImage.swift in Sources */, @@ -1415,6 +1419,7 @@ SDKROOT = auto; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; XROS_DEPLOYMENT_TARGET = 1.0; @@ -1540,6 +1545,7 @@ SDKROOT = auto; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; XROS_DEPLOYMENT_TARGET = 1.0; @@ -2103,15 +2109,15 @@ repositoryURL = "https://github.com/SDWebImage/SDWebImage"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.19.0; + minimumVersion = 5.19.7; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */ - 90D8318C2DB3CED6610DD38C /* XCLocalSwiftPackageReference "/Users/jerielng/swift-sdk/.build/braze/release/braze-swift-sdk" */ = { + B0CAEC5E933F542260FE1612 /* XCLocalSwiftPackageReference "/Users/daniel.hok/Desktop/swift-sdk/.build/braze/release/braze-swift-sdk" */ = { isa = XCLocalSwiftPackageReference; - relativePath = "/Users/jerielng/swift-sdk/.build/braze/release/braze-swift-sdk"; + relativePath = "/Users/daniel.hok/Desktop/swift-sdk/.build/braze/release/braze-swift-sdk"; }; /* End XCLocalSwiftPackageReference section */ diff --git a/Examples/Swift/Sources/Analytics/AuthenticationManager.swift b/Examples/Swift/Sources/Analytics/AuthenticationManager.swift index 72ab46eb06..d8b995df56 100644 --- a/Examples/Swift/Sources/Analytics/AuthenticationManager.swift +++ b/Examples/Swift/Sources/Analytics/AuthenticationManager.swift @@ -1,6 +1,7 @@ import BrazeKit import Foundation +@MainActor class AuthenticationManager { struct User { diff --git a/Examples/Swift/Sources/Analytics/Readme.swift b/Examples/Swift/Sources/Analytics/Readme.swift index 8c8ce5a0d9..3c492d460f 100644 --- a/Examples/Swift/Sources/Analytics/Readme.swift +++ b/Examples/Swift/Sources/Analytics/Readme.swift @@ -14,7 +14,8 @@ let readme = - Log purchases """ -let actions: [(String, String, (ReadmeViewController) -> Void)] = [ +@MainActor +let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [ ("Authenticate user", "Identify the user on Braze", { _ in authenticateUser() }), ("Present checkout", #"Log "open_checkout_controller" custom event"#, presentCheckout), ( @@ -26,8 +27,10 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ // MARK: - Internal +@MainActor let authenticationManager = AuthenticationManager() +@MainActor func authenticateUser() { let user = AuthenticationManager.User( id: UUID().uuidString, @@ -37,11 +40,13 @@ func authenticateUser() { authenticationManager.userDidLogin(user) } +@MainActor func presentCheckout(_ viewController: ReadmeViewController) { let (navigationController, _) = createCheckoutViewController() viewController.present(navigationController, animated: true, completion: nil) } +@MainActor func presentCheckoutAndPurchase(_ viewController: ReadmeViewController) { let (navigationController, checkoutViewController) = createCheckoutViewController() viewController.present(navigationController, animated: true) { @@ -49,6 +54,7 @@ func presentCheckoutAndPurchase(_ viewController: ReadmeViewController) { } } +@MainActor func createCheckoutViewController() -> (UINavigationController, CheckoutViewController) { let productsIds = [ UUID().uuidString, diff --git a/Examples/Swift/Sources/Common/ReadmeViewController.swift b/Examples/Swift/Sources/Common/ReadmeViewController.swift index 951f4094de..68b8f4175f 100644 --- a/Examples/Swift/Sources/Common/ReadmeViewController.swift +++ b/Examples/Swift/Sources/Common/ReadmeViewController.swift @@ -1,8 +1,9 @@ import UIKit +@MainActor final class ReadmeViewController: UITableViewController { - let actions: [(String, String, (ReadmeViewController) -> Void)] + let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] let readmeTextView: UITextView = { let textView = UITextView() @@ -34,7 +35,7 @@ final class ReadmeViewController: UITableViewController { return textView }() - init(readme: String, actions: [(String, String, (ReadmeViewController) -> Void)]) { + init(readme: String, actions: [(String, String, @MainActor (ReadmeViewController) -> Void)]) { self.actions = actions super.init(style: .grouped) @@ -107,6 +108,7 @@ final class ReadmeViewController: UITableViewController { // MARK: - AutoReadme +@MainActor private var _window: UIWindow? = { let readmeViewController = ReadmeViewController(readme: readme, actions: actions) let navigationController = UINavigationController(rootViewController: readmeViewController) diff --git a/Examples/Swift/Sources/ContentCardUI-Customization/AppDelegate.swift b/Examples/Swift/Sources/ContentCardUI-Customization/AppDelegate.swift index 2f048babf6..b607603376 100644 --- a/Examples/Swift/Sources/ContentCardUI-Customization/AppDelegate.swift +++ b/Examples/Swift/Sources/ContentCardUI-Customization/AppDelegate.swift @@ -217,6 +217,7 @@ extension AppDelegate { // MARK: - Helpers +@MainActor private var cards: [Braze.ContentCard] = [ classicPinned, classic, @@ -225,6 +226,7 @@ private var cards: [Braze.ContentCard] = [ captionedImage, ] +@MainActor private var navigationController: UINavigationController { UIApplication.shared.delegate!.window!!.rootViewController! as! UINavigationController } diff --git a/Examples/Swift/Sources/ContentCardUI-Customization/Readme.swift b/Examples/Swift/Sources/ContentCardUI-Customization/Readme.swift index 8ea340b63a..d1fafeff17 100644 --- a/Examples/Swift/Sources/ContentCardUI-Customization/Readme.swift +++ b/Examples/Swift/Sources/ContentCardUI-Customization/Readme.swift @@ -8,7 +8,8 @@ let readme = Customizations are applied when presenting the Content Cards UI in the `AppDelegate` methods. """ -let actions: [(String, String, (ReadmeViewController) -> Void)] = [ +@MainActor +let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [ ( "Default", "No customization.", @@ -58,6 +59,7 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ // MARK: - Internal +@MainActor let classicPinned: Braze.ContentCard = withContext( .classic( .init( @@ -70,6 +72,7 @@ let classicPinned: Braze.ContentCard = withContext( ) ) +@MainActor let classic: Braze.ContentCard = withContext( .classic( .init( @@ -81,6 +84,7 @@ let classic: Braze.ContentCard = withContext( ) ) +@MainActor let classicImage: Braze.ContentCard = withContext( .classicImage( .init( @@ -93,6 +97,7 @@ let classicImage: Braze.ContentCard = withContext( ) ) +@MainActor let imageOnly: Braze.ContentCard = withContext( .imageOnly( .init( @@ -103,6 +108,7 @@ let imageOnly: Braze.ContentCard = withContext( ) ) +@MainActor let captionedImage: Braze.ContentCard = withContext( .captionedImage( .init( @@ -115,38 +121,47 @@ let captionedImage: Braze.ContentCard = withContext( ) ) +@MainActor func noCustomization(_ viewController: ReadmeViewController) { AppDelegate.noCustomization() } +@MainActor func attributesCustomization(_ viewController: ReadmeViewController) { AppDelegate.attributesCustomization() } +@MainActor func tintColorCustomization(_ viewController: ReadmeViewController) { AppDelegate.tintColorCustomization() } +@MainActor func emptyStateCustomization(_ viewController: ReadmeViewController) { AppDelegate.emptyStateCustomization() } +@MainActor func subclassClassicImageCellCustomization(_ viewController: ReadmeViewController) { AppDelegate.subclassClassicImageCellCustomization() } +@MainActor func staticCardCustomization(_ viewController: ReadmeViewController) { AppDelegate.staticCardsCustomization() } +@MainActor func filterCardsCustomization(_ viewController: ReadmeViewController) { AppDelegate.filterCardsCustomization() } +@MainActor func transformCardsCustomization(_ viewController: ReadmeViewController) { AppDelegate.transformCardsCustomization() } +@MainActor func disableDarkThemeCustomization(_ viewController: ReadmeViewController) { AppDelegate.disableDarkThemeCustomization() } @@ -154,7 +169,8 @@ func disableDarkThemeCustomization(_ viewController: ReadmeViewController) { func withContext(_ card: Braze.ContentCard) -> Braze.ContentCard { var card = card - var loadImage: ((@escaping (Result) -> Void) -> Braze.Cancellable)? + var loadImage: + ((@MainActor @Sendable @escaping (Result) -> Void) -> Braze.Cancellable)? if let imageURL = card.imageURL { loadImage = { completion in DispatchQueue.main.async { @@ -168,21 +184,23 @@ func withContext(_ card: Braze.ContentCard) -> Braze.ContentCard { logImpression: {}, logClick: {}, processClickAction: { clickAction in - switch clickAction { - case .url(let url, let useWebView): - let alert = UIAlertController( - title: "Opening URL", - message: - """ - url: \(url) - useWebView: \(useWebView) - """, - preferredStyle: .actionSheet - ) - alert.addAction(.init(title: "Dismiss", style: .cancel)) - Braze.UIUtils.activeTopmostViewController?.present(alert, animated: true) - @unknown default: - print("Unknown click action: \(clickAction)") + DispatchQueue.main.async { + switch clickAction { + case .url(let url, let useWebView): + let alert = UIAlertController( + title: "Opening URL", + message: + """ + url: \(url) + useWebView: \(useWebView) + """, + preferredStyle: .actionSheet + ) + alert.addAction(.init(title: "Dismiss", style: .cancel)) + Braze.UIUtils.activeTopmostViewController?.present(alert, animated: true) + @unknown default: + print("Unknown click action: \(clickAction)") + } } }, logDismissed: {}, diff --git a/Examples/Swift/Sources/ContentCardUI/Readme.swift b/Examples/Swift/Sources/ContentCardUI/Readme.swift index 08294e9fe8..1e8489cc70 100644 --- a/Examples/Swift/Sources/ContentCardUI/Readme.swift +++ b/Examples/Swift/Sources/ContentCardUI/Readme.swift @@ -12,7 +12,8 @@ let readme = - Use SDWebImage to provide GIF support to the Braze UI components """ -let actions: [(String, String, (ReadmeViewController) -> Void)] = [ +@MainActor +let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [ ( "Push content cards view controller", "", @@ -32,14 +33,17 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ // MARK: - Internal +@MainActor func pushContentCardsViewController(_ viewController: ReadmeViewController) { (UIApplication.shared.delegate as? AppDelegate)?.pushContentCardsViewController() } +@MainActor func presentModalContentCardsViewController(_ viewController: ReadmeViewController) { (UIApplication.shared.delegate as? AppDelegate)?.presentModalContentCardsViewController() } +@MainActor func presentModalContentCardsViewControllerCustomized(_ viewController: ReadmeViewController) { (UIApplication.shared.delegate as? AppDelegate)? .presentModalContentCardsViewControllerCustomized() diff --git a/Examples/Swift/Sources/ContentCards-Custom-UI/Readme.swift b/Examples/Swift/Sources/ContentCards-Custom-UI/Readme.swift index 3d2b1197ac..df5b14e0cb 100644 --- a/Examples/Swift/Sources/ContentCards-Custom-UI/Readme.swift +++ b/Examples/Swift/Sources/ContentCards-Custom-UI/Readme.swift @@ -12,7 +12,8 @@ let readme = - UIViewController subclass presenting the content cards data in a table view. """ -let actions: [(String, String, (ReadmeViewController) -> Void)] = [ +@MainActor +let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [ ( "Print current Content Cards", "", @@ -47,26 +48,32 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ // MARK: - Internal +@MainActor func printCurrentContentCards(_ viewController: ReadmeViewController) { (UIApplication.shared.delegate as? AppDelegate)?.printCurrentContentCards() } +@MainActor func printCurrentContentCardsRaw(_ viewController: ReadmeViewController) { (UIApplication.shared.delegate as? AppDelegate)?.printCurrentContentCardsRaw() } +@MainActor func refreshContentCards(_ viewController: ReadmeViewController) { (UIApplication.shared.delegate as? AppDelegate)?.refreshContentCards() } +@MainActor func subscribeToContentCardsUpdates(_ viewController: ReadmeViewController) { (UIApplication.shared.delegate as? AppDelegate)?.subscribeToContentCardsUpdates() } +@MainActor func cancelContentCardsUpdatesSubscription(_ viewController: ReadmeViewController) { (UIApplication.shared.delegate as? AppDelegate)?.cancelContentCardsUpdatesSubscription() } +@MainActor func presentContentCardsInfoViewController(_ viewController: ReadmeViewController) { (UIApplication.shared.delegate as? AppDelegate)?.presentContentCardsInfoViewController() } diff --git a/Examples/Swift/Sources/InAppMessageUI-Customization/Readme.swift b/Examples/Swift/Sources/InAppMessageUI-Customization/Readme.swift index 3d176f4432..fe4630b34f 100644 --- a/Examples/Swift/Sources/InAppMessageUI-Customization/Readme.swift +++ b/Examples/Swift/Sources/InAppMessageUI-Customization/Readme.swift @@ -8,7 +8,8 @@ let readme = Customizations are applied in the `inAppMessage(_:prepareWith:)` delegate method implemented by the `AppDelegate`. """ -let actions: [(String, String, (ReadmeViewController) -> Void)] = [ +@MainActor +let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [ ( "Slideup - Default", "A non-customized slideup.", @@ -98,6 +99,7 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ // MARK: - Internal +@MainActor let slideup: Braze.InAppMessage = .slideup( .init( graphic: .image( @@ -108,6 +110,7 @@ let slideup: Braze.InAppMessage = .slideup( ) ) +@MainActor let modal: Braze.InAppMessage = .modal( .init( graphic: .image( @@ -121,6 +124,7 @@ let modal: Braze.InAppMessage = .modal( ) ) +@MainActor let modalImage: Braze.InAppMessage = .modalImage( .init( imageURL: .mockImage( @@ -130,6 +134,7 @@ let modalImage: Braze.InAppMessage = .modalImage( ) ) +@MainActor let full: Braze.InAppMessage = .full( .init( imageURL: .mockImage( @@ -142,6 +147,7 @@ let full: Braze.InAppMessage = .full( ) ) +@MainActor let fullImage: Braze.InAppMessage = .fullImage( .init( imageURL: .mockImage( @@ -151,98 +157,116 @@ let fullImage: Braze.InAppMessage = .fullImage( ) ) +@MainActor func slideupDefault(_ viewController: ReadmeViewController) { present(message: slideup) } +@MainActor func slideupAttributes(_ viewController: ReadmeViewController) { var message = slideup message.extras = ["customization": "slideup-attributes"] present(message: message) } +@MainActor func slideupFullWidth(_ viewController: ReadmeViewController) { var message = slideup message.extras = ["customization": "slideup-full-width"] present(message: message) } +@MainActor func slideupConfirmButton(_ viewController: ReadmeViewController) { var message = slideup message.extras = ["customization": "slideup-confirm-button"] present(message: message) } +@MainActor func modalDefault(_ viewController: ReadmeViewController) { present(message: modal) } +@MainActor func modalAttributes(_ viewController: ReadmeViewController) { var message = modal message.extras = ["customization": "modal-attributes"] present(message: message) } +@MainActor func modalDismissOnBackgroundTap(_ viewController: ReadmeViewController) { var message = modal message.extras = ["customization": "modal-dismiss-on-background-tap"] present(message: message) } +@MainActor func modalButtonFont(_ viewController: ReadmeViewController) { var message = modal message.extras = ["customization": "modal-button-font"] present(message: message) } +@MainActor func modalImageDefault(_ viewController: ReadmeViewController) { present(message: modalImage) } +@MainActor func modalImageAttributes(_ viewController: ReadmeViewController) { var message = modalImage message.extras = ["customization": "modal-image-attributes"] present(message: message) } +@MainActor func modalImageDismissOnBackgroundTap(_ viewController: ReadmeViewController) { var message = modalImage message.extras = ["customization": "modal-image-dismiss-on-background-tap"] present(message: message) } +@MainActor func fullDefault(_ viewController: ReadmeViewController) { present(message: full) } +@MainActor func fullAttributes(_ viewController: ReadmeViewController) { var message = full message.extras = ["customization": "full-attributes"] present(message: message) } +@MainActor func fullDismissOnBackgroundTap(_ viewController: ReadmeViewController) { var message = full message.extras = ["customization": "full-dismiss-on-background-tap"] present(message: message) } +@MainActor func fullImageDefault(_ viewController: ReadmeViewController) { present(message: fullImage) } +@MainActor func fullImageAttributes(_ viewController: ReadmeViewController) { var message = fullImage message.extras = ["customization": "full-image-attributes"] present(message: message) } +@MainActor func fullImageDismissOnBackgroundTap(_ viewController: ReadmeViewController) { var message = fullImage message.extras = ["customization": "full-image-dismiss-on-background-tap"] present(message: message) } +@MainActor private func present(message: Braze.InAppMessage) { AppDelegate.braze?.inAppMessagePresenter?.present(message: message) } diff --git a/Examples/Swift/Sources/InAppMessageUI/Readme.swift b/Examples/Swift/Sources/InAppMessageUI/Readme.swift index 94b5be71a9..23748e124d 100644 --- a/Examples/Swift/Sources/InAppMessageUI/Readme.swift +++ b/Examples/Swift/Sources/InAppMessageUI/Readme.swift @@ -12,7 +12,8 @@ let readme = - Use SDWebImage to provide GIF support to the Braze UI components """ -let actions: [(String, String, (ReadmeViewController) -> Void)] = [ +@MainActor +let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [ ( "Present local slideup in-app message", "", @@ -27,6 +28,7 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ // MARK: - Internal +@MainActor func localSlideup(_ viewController: ReadmeViewController) { let slideup: Braze.InAppMessage = .slideup( .init( @@ -37,6 +39,7 @@ func localSlideup(_ viewController: ReadmeViewController) { AppDelegate.braze?.inAppMessagePresenter?.present(message: slideup) } +@MainActor func localModal(_ viewController: ReadmeViewController) { let modal: Braze.InAppMessage = .modal( .init( diff --git a/Examples/Swift/Sources/InAppMessages-Custom-UI/InAppMessageInfoViewController.swift b/Examples/Swift/Sources/InAppMessages-Custom-UI/InAppMessageInfoViewController.swift index 82e8b5b655..06a3b8ec31 100644 --- a/Examples/Swift/Sources/InAppMessages-Custom-UI/InAppMessageInfoViewController.swift +++ b/Examples/Swift/Sources/InAppMessages-Custom-UI/InAppMessageInfoViewController.swift @@ -1,6 +1,7 @@ import BrazeKit import UIKit +@MainActor final class InAppMessageInfoViewController: UITableViewController { // Represents an in-app message property diff --git a/Examples/Swift/Sources/InAppMessages-Custom-UI/Readme.swift b/Examples/Swift/Sources/InAppMessages-Custom-UI/Readme.swift index 072046cf11..79f2f66abf 100644 --- a/Examples/Swift/Sources/InAppMessages-Custom-UI/Readme.swift +++ b/Examples/Swift/Sources/InAppMessages-Custom-UI/Readme.swift @@ -11,7 +11,8 @@ let readme = - Explains how to use the `Braze.InAppMessage` data model and present the message in a custom view controller. """ -let actions: [(String, String, (ReadmeViewController) -> Void)] = [ +@MainActor +let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [ ( "Present local slideup in-app message", "", @@ -26,6 +27,7 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ // MARK: - Internal +@MainActor func localSlideup(_ viewController: ReadmeViewController) { let slideup: Braze.InAppMessage = .slideup( .init( @@ -40,6 +42,7 @@ func localSlideup(_ viewController: ReadmeViewController) { AppDelegate.braze?.inAppMessagePresenter?.present(message: slideup) } +@MainActor func localModal(_ viewController: ReadmeViewController) { let modal: Braze.InAppMessage = .modal( .init( diff --git a/Examples/Swift/Sources/Location/Readme.swift b/Examples/Swift/Sources/Location/Readme.swift index 92ed68cce3..90af503d1c 100644 --- a/Examples/Swift/Sources/Location/Readme.swift +++ b/Examples/Swift/Sources/Location/Readme.swift @@ -9,26 +9,31 @@ let readme = """ #if os(iOS) - let actions: [(String, String, (ReadmeViewController) -> Void)] = [ + @MainActor + let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [ (#"Request "always" authorization"#, "", requestAlwaysAuthorization), (#"Request "when in use" authorization"#, "", requestWhenInUseAuthorization), ] #elseif os(tvOS) || os(visionOS) - let actions: [(String, String, (ReadmeViewController) -> Void)] = [ + @MainActor + let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [ (#"Request "when in use" authorization"#, "", requestWhenInUseAuthorization) ] #endif // MARK: - Internal +@MainActor let locationManager = CLLocationManager() #if os(iOS) + @MainActor func requestAlwaysAuthorization(_ viewController: ReadmeViewController) { locationManager.requestAlwaysAuthorization() } #endif +@MainActor func requestWhenInUseAuthorization(_ viewController: ReadmeViewController) { locationManager.requestWhenInUseAuthorization() } diff --git a/Examples/Swift/Sources/PushNotifications-Automatic/Readme.swift b/Examples/Swift/Sources/PushNotifications-Automatic/Readme.swift index 8dc8805903..d20912d458 100644 --- a/Examples/Swift/Sources/PushNotifications-Automatic/Readme.swift +++ b/Examples/Swift/Sources/PushNotifications-Automatic/Readme.swift @@ -14,4 +14,5 @@ let readme = - Braze Push Story implementation """ -let actions: [(String, String, (ReadmeViewController) -> Void)] = [] +@MainActor +let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [] diff --git a/Examples/Swift/Sources/PushNotifications-DelayedInitialization/Readme.swift b/Examples/Swift/Sources/PushNotifications-DelayedInitialization/Readme.swift index a4b922250d..70cd504a93 100644 --- a/Examples/Swift/Sources/PushNotifications-DelayedInitialization/Readme.swift +++ b/Examples/Swift/Sources/PushNotifications-DelayedInitialization/Readme.swift @@ -13,7 +13,8 @@ let readme = - Braze Push Story implementation """ -let actions: [(String, String, (ReadmeViewController) -> Void)] = [ +@MainActor +let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [ ( "Initialize Braze", "", @@ -23,6 +24,7 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ // MARK: - Internal +@MainActor func initializeBraze(_ viewController: ReadmeViewController) { AppDelegate.initializeBraze() diff --git a/Examples/Swift/Sources/PushNotifications-Manual/AppDelegate+Xcode16.swift b/Examples/Swift/Sources/PushNotifications-Manual/AppDelegate+Xcode16.swift new file mode 100644 index 0000000000..d7dc529693 --- /dev/null +++ b/Examples/Swift/Sources/PushNotifications-Manual/AppDelegate+Xcode16.swift @@ -0,0 +1,47 @@ +import UserNotifications + +// Important: `UNUserNotificationCenterDelegate` is not yet annotated for Swift Concurrency. We use +// the `@preconcurrency` attribute to suppress concurrency warnings. +// The `@preconcurrency` attribute on protocol conformance is only available starting with Xcode 16. +#if compiler(>=6.0) + extension AppDelegate: @preconcurrency UNUserNotificationCenterDelegate { + + // - Add support for push notifications + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + if let braze = AppDelegate.braze, + braze.notifications.handleUserNotification( + response: response, + withCompletionHandler: completionHandler + ) + { + return + } + completionHandler() + } + + // - Add support for displaying push notification when the app is currently running in the + // foreground + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + if let braze = AppDelegate.braze { + braze.notifications.handleForegroundNotification(notification: notification) + } + + if #available(iOS 14, *) { + completionHandler([.list, .banner]) + } else { + completionHandler(.alert) + } + } + + } +#endif diff --git a/Examples/Swift/Sources/PushNotifications-Manual/AppDelegate.swift b/Examples/Swift/Sources/PushNotifications-Manual/AppDelegate.swift index 52037336fa..c9fa10c7af 100644 --- a/Examples/Swift/Sources/PushNotifications-Manual/AppDelegate.swift +++ b/Examples/Swift/Sources/PushNotifications-Manual/AppDelegate.swift @@ -76,43 +76,48 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } -extension AppDelegate: UNUserNotificationCenterDelegate { - - // - Add support for push notifications - - func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void - ) { - if let braze = AppDelegate.braze, - braze.notifications.handleUserNotification( - response: response, - withCompletionHandler: completionHandler - ) - { - return +// When using Xcode 16+, see the AppDelegate+Xcode16.swift file for the +// `UNUserNotificationCenterDelegate` conformance. Xcode 16+ provides tools to suppress concurrency +// related warnings. +#if compiler(<6.0) + extension AppDelegate: UNUserNotificationCenterDelegate { + + // - Add support for push notifications + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + if let braze = AppDelegate.braze, + braze.notifications.handleUserNotification( + response: response, + withCompletionHandler: completionHandler + ) + { + return + } + completionHandler() } - completionHandler() - } - - // - Add support for displaying push notification when the app is currently running in the - // foreground - func userNotificationCenter( - _ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void - ) { - if let braze = AppDelegate.braze { - braze.notifications.handleForegroundNotification(notification: notification) + // - Add support for displaying push notification when the app is currently running in the + // foreground + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + if let braze = AppDelegate.braze { + braze.notifications.handleForegroundNotification(notification: notification) + } + + if #available(iOS 14, *) { + completionHandler([.list, .banner]) + } else { + completionHandler(.alert) + } } - if #available(iOS 14, *) { - completionHandler([.list, .banner]) - } else { - completionHandler(.alert) - } } - -} +#endif diff --git a/Examples/Swift/Sources/PushNotifications-Manual/Readme.swift b/Examples/Swift/Sources/PushNotifications-Manual/Readme.swift index 8902210a6a..9f6ce7fb08 100644 --- a/Examples/Swift/Sources/PushNotifications-Manual/Readme.swift +++ b/Examples/Swift/Sources/PushNotifications-Manual/Readme.swift @@ -17,4 +17,5 @@ let readme = See the PushNotifications-Automatic example app for a configuration based integration. """ -let actions: [(String, String, (ReadmeViewController) -> Void)] = [] +@MainActor +let actions: [(String, String, @MainActor (ReadmeViewController) -> Void)] = [] diff --git a/Examples/Swift/Sources/PushNotifications/Info.plist b/Examples/Swift/Sources/PushNotifications/Info.plist new file mode 100644 index 0000000000..d7f8ec8ca2 --- /dev/null +++ b/Examples/Swift/Sources/PushNotifications/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + PushNotifications + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + LaunchScreen + + diff --git a/Examples/Swift/Sources/PushNotifications/PushNotifications.entitlements b/Examples/Swift/Sources/PushNotifications/PushNotifications.entitlements new file mode 100644 index 0000000000..9cad402b51 --- /dev/null +++ b/Examples/Swift/Sources/PushNotifications/PushNotifications.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.security.application-groups + + group.com.braze.PushNotifications.PushStories + + + diff --git a/Examples/Swift/manual-integration-setup.sh b/Examples/Swift/manual-integration-setup.sh index 8292ceab62..7f388cb6bd 100755 --- a/Examples/Swift/manual-integration-setup.sh +++ b/Examples/Swift/manual-integration-setup.sh @@ -20,7 +20,7 @@ if [ ! -f "manual-integration-setup.sh" ]; then fi # Constants -url="https://github.com/braze-inc/braze-swift-sdk/releases/download/10.3.1/braze-swift-sdk-prebuilt.zip" +url="https://github.com/braze-inc/braze-swift-sdk/releases/download/11.0.0/braze-swift-sdk-prebuilt.zip" echo "→" "Cleaning up" rm -rf braze-swift-sdk-prebuilt diff --git a/Package.swift b/Package.swift index ebe17db725..a28bd7a67d 100644 --- a/Package.swift +++ b/Package.swift @@ -42,15 +42,15 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.19.0"), + .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.19.7"), /* ${dependencies-start} */ /* ${dependencies-end} */ ], targets: [ .binaryTarget( name: "BrazeKit", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/10.3.1/BrazeKit.zip", - checksum: "5bba5938771c81c6bbc1909098f749487c901f950ab16c84d01bcf38e2b8fe82" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/11.0.0/BrazeKit.zip", + checksum: "0a236bea81a47f3a3835d09c1e631d45039bd4baefca1a791b3c0085a415145f" ), .target( name: "BrazeKitResources", @@ -67,8 +67,8 @@ let package = Package( ), .binaryTarget( name: "BrazeLocation", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/10.3.1/BrazeLocation.zip", - checksum: "cd85ecb7157f7133461794d6609ef2c052ed983d038c75a78f1cf0c120f819fd" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/11.0.0/BrazeLocation.zip", + checksum: "4962245ad3fc46efe1ae4d08926ce7cedc6bb54db42fbc37824a4f316cd3bfbe" ), .target( name: "BrazeLocationResources", @@ -78,13 +78,13 @@ let package = Package( ), .binaryTarget( name: "BrazeNotificationService", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/10.3.1/BrazeNotificationService.zip", - checksum: "178183799f3b4f6b2f4b484152ec2cf577e761e7b43da3002a754a179ba87290" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/11.0.0/BrazeNotificationService.zip", + checksum: "dfbcbdb4e37789a229743be1009d9f00811cd51b80d6c0d00f4ded2e75c85d76" ), .binaryTarget( name: "BrazePushStory", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/10.3.1/BrazePushStory.zip", - checksum: "bf807697dbe65acb25ae780d2815a39b32742592998f80f5120e3af7ff1b8132" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/11.0.0/BrazePushStory.zip", + checksum: "8e3579612aed0d7c16092683bd4fe99fe3ba6e8aa8a1bec103dd001b5e0891d5" ), .target( name: "BrazePushStoryResources", diff --git a/README.md b/README.md index 7244de3337..a6c3de14b7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- Version: 10.3.1 + Version: 11.0.0 ) -> Void) -> Void)? = nil, subscribe: ((@escaping ([Braze.ContentCard]) -> Void) -> Braze.Cancellable)? = nil, lastUpdate: Date? = nil, - attributes: ViewController.Attributes = .defaults, + attributes: ViewController.Attributes? = nil, title: String = "" ) { + let attributes = attributes ?? .defaults + viewController = .init( initialCards: initialCards, refresh: refresh, diff --git a/Sources/BrazeUI/ContentCardUI/ContentCardUISwiftUI.swift b/Sources/BrazeUI/ContentCardUI/ContentCardUISwiftUI.swift index 6edebe6069..cd30b242e3 100644 --- a/Sources/BrazeUI/ContentCardUI/ContentCardUISwiftUI.swift +++ b/Sources/BrazeUI/ContentCardUI/ContentCardUISwiftUI.swift @@ -11,6 +11,7 @@ /// A SwiftUI view which displays Braze Content Cards. @available(iOS 13.0, *) + @MainActor public struct ContentCardsView: UIViewControllerRepresentable { /// The attributes supported by the view. @@ -32,11 +33,12 @@ /// - attributes: An attributes struct allowing customization of the view and its cells. public init( braze: Braze?, - shouldProcess: @escaping (Braze.ContentCard.ClickAction, Braze.ContentCard) -> Bool = { - _, _ in true - }, - attributes: Attributes = .defaults + shouldProcess: @MainActor @escaping (Braze.ContentCard.ClickAction, Braze.ContentCard) + -> Bool = { _, _ in true }, + attributes: Attributes? = nil ) { + let attributes = attributes ?? .defaults + self.braze = braze self.shouldProcess = shouldProcess self.attributes = attributes @@ -70,7 +72,7 @@ @available(iOS 13.0, *) extension ContentCardsView { - public class Coordinator: BrazeContentCardUIViewControllerDelegate { + public final class Coordinator: BrazeContentCardUIViewControllerDelegate { weak var braze: Braze? var shouldProcess: (Braze.ContentCard.ClickAction, Braze.ContentCard) -> Bool diff --git a/Sources/BrazeUI/ContentCardUI/ContentCardUIViewController.swift b/Sources/BrazeUI/ContentCardUI/ContentCardUIViewController.swift index 2dbc9d2c81..4c332c13dc 100644 --- a/Sources/BrazeUI/ContentCardUI/ContentCardUIViewController.swift +++ b/Sources/BrazeUI/ContentCardUI/ContentCardUIViewController.swift @@ -39,6 +39,7 @@ extension BrazeContentCardUI { /// The attributes supported by the view controller. /// /// Attributes allows customizing the view controller and its associated cells. + @MainActor public struct Attributes { /// The _cell identifier_ to _cell class_ map. @@ -79,7 +80,9 @@ extension BrazeContentCardUI { /// Closure allowing the modification of the content cards list presented. /// /// This closure is executed every time the controller receives content cards to display. - public var transform: ([Braze.ContentCard]) -> [Braze.ContentCard] = { $0 } + public var transform: @MainActor @Sendable ([Braze.ContentCard]) -> [Braze.ContentCard] = { + $0 + } /// The message displayed when there is no content cards available. public var emptyStateMessage: String = localize( @@ -156,7 +159,9 @@ extension BrazeContentCardUI { /// - braze: The Braze instance. /// - attributes: An attributes struct allowing customization of the table view controller /// and its cells. - public convenience init(braze: Braze, attributes: Attributes = .defaults) { + public convenience init(braze: Braze, attributes: Attributes? = nil) { + let attributes = attributes ?? .defaults + self.init( initialCards: braze.contentCards.cards, refresh: { [weak braze] fulfill in @@ -187,8 +192,10 @@ extension BrazeContentCardUI { refresh: ((@escaping (Result<[Braze.ContentCard], Error>) -> Void) -> Void)? = nil, subscribe: ((@escaping ([Braze.ContentCard]) -> Void) -> Braze.Cancellable)? = nil, lastUpdate: Date? = nil, - attributes: Attributes = .defaults + attributes: Attributes? = nil ) { + let attributes = attributes ?? .defaults + self.cards = attributes.transform(initialCards) self.refresh = refresh self.subscribe = subscribe @@ -402,7 +409,7 @@ extension BrazeContentCardUI { open func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { indexPaths - .compactMap(card(at:)) + .compactMap { card(at: $0) } .forEach { loadImage(card: $0) } } @@ -410,8 +417,8 @@ extension BrazeContentCardUI { _ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath] ) { indexPaths - .compactMap(card(at:)) - .forEach(cancelLoadImage(card:)) + .compactMap { card(at: $0) } + .forEach { cancelLoadImage(card: $0) } } // MARK: - Subscribe @@ -510,7 +517,10 @@ extension BrazeContentCardUI { guard let self = self, let visibleIndexPaths = self.tableView.indexPathsForVisibleRows else { return [] } - let ids = visibleIndexPaths.compactMap(self.card(at:)).map(\.id) + let ids = + visibleIndexPaths + .compactMap { self.card(at: $0) } + .map(\.id) return ids }, visibleForInterval: { [weak self] id in diff --git a/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate+ObjC.swift b/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate+ObjC.swift index f3955b8631..0e08e03cd3 100644 --- a/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate+ObjC.swift +++ b/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate+ObjC.swift @@ -20,6 +20,7 @@ public protocol _OBJC_BrazeContentCardUIViewControllerDelegate: AnyObject { /// - card: The Content Card. /// - Returns: `true` to let Braze process the click action, `false` otherwise @objc(contentCardController:shouldProcess:card:) + @MainActor optional func _objc_contentCard( _ controller: BrazeContentCardUI.ViewController, shouldProcess url: URL, @@ -34,8 +35,13 @@ final class _OBJC_BrazeContentCardUIViewControllerDelegateWrapper: BrazeContentCardUIViewControllerDelegate { - /// Property used as a unique key for the wrapper lifecycle behavior. - private static var wrapperKey: Void? + // nonisolated(unsafe) attribute for global variable is only available in Xcode 15.3 and later. + #if compiler(>=5.10) + /// Property used as a unique key for the wrapper lifecycle behavior. + nonisolated(unsafe) private static var wrapperKey: Void? + #else + private static var wrapperKey: Void? + #endif /// The ObjC content card UI view controller delegate. weak var delegate: _OBJC_BrazeContentCardUIViewControllerDelegate? diff --git a/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate.swift b/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate.swift index 6b3a5b8eff..b5bef49338 100644 --- a/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate.swift +++ b/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate.swift @@ -18,6 +18,7 @@ public protocol BrazeContentCardUIViewControllerDelegate: AnyObject { /// - clickAction: The click action. /// - card: The Content Card. /// - Returns: `true` to let Braze process the click action, `false` otherwise + @MainActor func contentCard( _ controller: BrazeContentCardUI.ViewController, shouldProcess clickAction: Braze.ContentCard.ClickAction, diff --git a/Sources/BrazeUI/Dependencies/Align.swift b/Sources/BrazeUI/Dependencies/Align.swift index 686c99311c..1dfff654a9 100644 --- a/Sources/BrazeUI/Dependencies/Align.swift +++ b/Sources/BrazeUI/Dependencies/Align.swift @@ -9,6 +9,7 @@ import UIKit protocol LayoutItem { // `UIView`, `UILayoutGuide` + @MainActor var superview: UIView? { get } } @@ -20,6 +21,7 @@ import AppKit protocol LayoutItem { // `NSView`, `NSLayoutGuide` + @MainActor var superview: NSView? { get } } @@ -160,18 +162,21 @@ func * (anchor: Anchor, multiplier: CGFloat) -> Anchor( _ anchor: Anchor, constant: CGFloat = 0 ) -> NSLayoutConstraint { Constraints.add(self, anchor, constant: constant, relation: .equal) } + @MainActor @discardableResult func greaterThanOrEqual( _ anchor: Anchor, constant: CGFloat = 0 ) -> NSLayoutConstraint { Constraints.add(self, anchor, constant: constant, relation: .greaterThanOrEqual) } + @MainActor @discardableResult func lessThanOrEqual( _ anchor: Anchor, constant: CGFloat = 0 ) -> NSLayoutConstraint { @@ -183,18 +188,21 @@ extension Anchor where Type: AnchorType.Alignment { extension Anchor where Type: AnchorType.Dimension { /// Adds a constraint that defines the anchors' attributes as equal to each other. + @MainActor @discardableResult func equal( _ anchor: Anchor, constant: CGFloat = 0 ) -> NSLayoutConstraint { Constraints.add(self, anchor, constant: constant, relation: .equal) } + @MainActor @discardableResult func greaterThanOrEqual( _ anchor: Anchor, constant: CGFloat = 0 ) -> NSLayoutConstraint { Constraints.add(self, anchor, constant: constant, relation: .greaterThanOrEqual) } + @MainActor @discardableResult func lessThanOrEqual( _ anchor: Anchor, constant: CGFloat = 0 ) -> NSLayoutConstraint { @@ -205,21 +213,25 @@ extension Anchor where Type: AnchorType.Dimension { // MARK: - Anchors (AnchorType.Dimension) extension Anchor where Type: AnchorType.Dimension { + @MainActor @discardableResult func equal(_ constant: CGFloat) -> NSLayoutConstraint { Constraints.add(item: item, attribute: attribute, relatedBy: .equal, constant: constant) } + @MainActor @discardableResult func greaterThanOrEqual(_ constant: CGFloat) -> NSLayoutConstraint { Constraints.add( item: item, attribute: attribute, relatedBy: .greaterThanOrEqual, constant: constant) } + @MainActor @discardableResult func lessThanOrEqual(_ constant: CGFloat) -> NSLayoutConstraint { Constraints.add( item: item, attribute: attribute, relatedBy: .lessThanOrEqual, constant: constant) } /// Clamps the dimension of a view to the given limiting range. + @MainActor @discardableResult func clamp(to limits: ClosedRange) -> [NSLayoutConstraint] { [greaterThanOrEqual(limits.lowerBound), lessThanOrEqual(limits.upperBound)] } @@ -229,6 +241,7 @@ extension Anchor where Type: AnchorType.Dimension { extension Anchor where Type: AnchorType.Edge { /// Pins the edge to the respected edges of the given container. + @MainActor @discardableResult func pin(to container: LayoutItem? = nil, inset: CGFloat = 0) -> NSLayoutConstraint { @@ -239,6 +252,7 @@ extension Anchor where Type: AnchorType.Edge { } /// Adds spacing between the current anchors. + @MainActor @discardableResult func spacing( _ spacing: CGFloat, to anchor: Anchor, relation: NSLayoutConstraint.Relation = .equal @@ -257,6 +271,7 @@ extension Anchor where Type: AnchorType.Edge { extension Anchor where Type: AnchorType.Center { /// Aligns the axis with a superview axis. + @MainActor @discardableResult func align(offset: CGFloat = 0) -> NSLayoutConstraint { Constraints.add(self, toItem: item.superview!, attribute: attribute, constant: offset) } @@ -308,22 +323,26 @@ struct AnchorCollectionEdges { // MARK: Core API + @MainActor @discardableResult func equal(_ item2: LayoutItem, insets: EdgeInsets = .zero) -> [NSLayoutConstraint] { pin(to: item2, insets: insets) } + @MainActor @discardableResult func lessThanOrEqual(_ item2: LayoutItem, insets: EdgeInsets = .zero) -> [NSLayoutConstraint] { pin(to: item2, insets: insets, axis: nil, alignment: .center, isCenteringEnabled: false) } + @MainActor @discardableResult func equal(_ item2: LayoutItem, insets: CGFloat) -> [NSLayoutConstraint] { pin(to: item2, insets: EdgeInsets(top: insets, left: insets, bottom: insets, right: insets)) } + @MainActor @discardableResult func lessThanOrEqual(_ item2: LayoutItem, insets: CGFloat) -> [NSLayoutConstraint] { @@ -343,6 +362,7 @@ struct AnchorCollectionEdges { /// `.trailing` (and `.centerX` if needed) attributes are used. `nil` by default /// - parameter alignment: `.fill` by default, see `Alignment` for a list of /// the available options. + @MainActor @discardableResult func pin( to item2: LayoutItem? = nil, insets: CGFloat, axis: Axis? = nil, alignment: Alignment = .fill ) -> [NSLayoutConstraint] { @@ -360,6 +380,7 @@ struct AnchorCollectionEdges { /// `.trailing` (and `.centerX` if needed) attributes are used. `nil` by default /// - parameter alignment: `.fill` by default, see `Alignment` for a list of /// the available options. + @MainActor @discardableResult func pin( to item2: LayoutItem? = nil, insets: EdgeInsets = .zero, axis: Axis? = nil, alignment: Alignment = .fill @@ -367,6 +388,7 @@ struct AnchorCollectionEdges { pin(to: item2, insets: insets, axis: axis, alignment: alignment, isCenteringEnabled: true) } + @MainActor private func pin( to item2: LayoutItem?, insets: EdgeInsets, axis: Axis?, alignment: Alignment, isCenteringEnabled: Bool @@ -424,6 +446,7 @@ struct AnchorCollectionCenter { // MARK: Core API + @MainActor @discardableResult func equal(_ item2: Item, offset: CGPoint = .zero) -> [NSLayoutConstraint] { @@ -433,6 +456,7 @@ struct AnchorCollectionCenter { ] } + @MainActor @discardableResult func greaterThanOrEqual( _ item2: Item, offset: CGPoint = .zero ) -> [NSLayoutConstraint] { @@ -442,6 +466,7 @@ struct AnchorCollectionCenter { ] } + @MainActor @discardableResult func lessThanOrEqual(_ item2: Item, offset: CGPoint = .zero) -> [NSLayoutConstraint] { @@ -454,11 +479,13 @@ struct AnchorCollectionCenter { // MARK: Semantic API /// Centers the view in the superview. + @MainActor @discardableResult func align() -> [NSLayoutConstraint] { [x.align(), y.align()] } /// Makes the axis equal to the other collection of axis. + @MainActor @discardableResult func align(with item: Item) -> [NSLayoutConstraint] { [x.equal(item.anchors.centerX), y.equal(item.anchors.centerY)] } @@ -473,21 +500,25 @@ struct AnchorCollectionSize { // MARK: Core API /// Set the size of item. + @MainActor @discardableResult func equal(_ size: CGSize) -> [NSLayoutConstraint] { [width.equal(size.width), height.equal(size.height)] } /// Set the size of item. + @MainActor @discardableResult func greaterThanOrEqul(_ size: CGSize) -> [NSLayoutConstraint] { [width.greaterThanOrEqual(size.width), height.greaterThanOrEqual(size.height)] } /// Set the size of item. + @MainActor @discardableResult func lessThanOrEqual(_ size: CGSize) -> [NSLayoutConstraint] { [width.lessThanOrEqual(size.width), height.lessThanOrEqual(size.height)] } /// Makes the size of the item equal to the size of the other item. + @MainActor @discardableResult func equal( _ item: Item, insets: CGSize = .zero, multiplier: CGFloat = 1 ) -> [NSLayoutConstraint] { @@ -497,6 +528,7 @@ struct AnchorCollectionSize { ] } + @MainActor @discardableResult func greaterThanOrEqual( _ item: Item, insets: CGSize = .zero, multiplier: CGFloat = 1 ) -> [NSLayoutConstraint] { @@ -506,6 +538,7 @@ struct AnchorCollectionSize { ] } + @MainActor @discardableResult func lessThanOrEqual( _ item: Item, insets: CGSize = .zero, multiplier: CGFloat = 1 ) -> [NSLayoutConstraint] { @@ -539,6 +572,7 @@ final class Constraints: Collection { /// changes to the constraints before they are installed (e.g. change `priority`). /// /// - parameter activate: Set to `false` to disable automatic activation of constraints. + @MainActor @discardableResult init(activate: Bool = true, _ closure: () -> Void) { Constraints.stack.append(self) closure() // create constraints @@ -549,11 +583,13 @@ final class Constraints: Collection { // MARK: Activate /// Activates each constraint in the reciever. + @MainActor func activate() { NSLayoutConstraint.activate(constraints) } /// Deactivates each constraint in the reciever. + @MainActor func deactivate() { NSLayoutConstraint.deactivate(constraints) } @@ -561,6 +597,7 @@ final class Constraints: Collection { // MARK: Adding Constraints /// Creates and automatically installs a constraint. + @MainActor static func add( item item1: Any, attribute attr1: NSLayoutConstraint.Attribute, relatedBy relation: NSLayoutConstraint.Relation = .equal, toItem item2: Any? = nil, @@ -581,6 +618,7 @@ final class Constraints: Collection { } /// Creates and automatically installs a constraint between two anchors. + @MainActor static func add( _ lhs: Anchor, _ rhs: Anchor, constant: CGFloat = 0, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal @@ -593,6 +631,7 @@ final class Constraints: Collection { /// Creates and automatically installs a constraint between an anchor and /// a given item. + @MainActor static func add( _ lhs: Anchor, toItem item2: Any?, attribute attr2: NSLayoutConstraint.Attribute?, constant: CGFloat = 0, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal @@ -602,8 +641,10 @@ final class Constraints: Collection { attribute: attr2, multiplier: multiplier / lhs.multiplier, constant: constant - lhs.offset) } + @MainActor private static var stack = [Constraints]() // this is what enabled constraint auto-installing + @MainActor private static func install(_ constraint: NSLayoutConstraint) { if let group = stack.last { group.constraints.append(constraint) @@ -614,18 +655,21 @@ final class Constraints: Collection { } extension Constraints { + @MainActor @discardableResult convenience init( for a: A, _ closure: (LayoutAnchors) -> Void ) { self.init { closure(a.anchors) } } + @MainActor @discardableResult convenience init( for a: A, _ b: B, _ closure: (LayoutAnchors, LayoutAnchors) -> Void ) { self.init { closure(a.anchors, b.anchors) } } + @MainActor @discardableResult convenience init( for a: A, _ b: B, _ c: C, _ closure: (LayoutAnchors, LayoutAnchors, LayoutAnchors) -> Void @@ -633,6 +677,7 @@ extension Constraints { self.init { closure(a.anchors, b.anchors, c.anchors) } } + @MainActor @discardableResult convenience init( for a: A, _ b: B, _ c: C, _ d: D, _ closure: (LayoutAnchors, LayoutAnchors, LayoutAnchors, LayoutAnchors) -> Void diff --git a/Sources/BrazeUI/Dependencies/ConcurrencyHelper.swift b/Sources/BrazeUI/Dependencies/ConcurrencyHelper.swift new file mode 100644 index 0000000000..164cfda7b4 --- /dev/null +++ b/Sources/BrazeUI/Dependencies/ConcurrencyHelper.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Method wrapping deinitialization work in a main actor context. +/// +/// This method workaround the error _Call to main actor-isolated instance method '...' in a +/// synchronous nonisolated context_ when performing cleanup in a MainActor bound deinit method. +/// In that context, `deinit` is not isolated to the MainActor by Swift concurrency, but can be +/// guaranteed to the main thread for historical reasons (e.g. UIKit lifecycle). +/// +/// - SeeAlso: [Deinit and MainActor](https://archive.is/whA6f) and +/// [Isolated synchronous deinit](https://archive.is/9MfIY) +/// +/// - Important: This method should only ever be used in the `deinit` method of a class that is +/// guaranteed to be deinitialized on the main thread (e.g. `UIView`, +/// `UIViewController`). +/// +/// - Parameter work: The deinitialization work to perform. +func isolatedMainActorDeinit(work: @MainActor @Sendable @escaping () -> Void) { + #if compiler(>=5.10) + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + MainActor.assumeIsolated { work() } + } else { + DispatchQueue.main.async { work() } + } + #else + if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { + MainActor.assumeIsolated { work() } + } else { + DispatchQueue.main.async { work() } + } + #endif +} diff --git a/Sources/BrazeUI/Dependencies/GIFViewProvider+ObjC.swift b/Sources/BrazeUI/Dependencies/GIFViewProvider+ObjC.swift index 4839438de4..99e2a17c15 100644 --- a/Sources/BrazeUI/Dependencies/GIFViewProvider+ObjC.swift +++ b/Sources/BrazeUI/Dependencies/GIFViewProvider+ObjC.swift @@ -15,6 +15,7 @@ import UIKit /// GIFViewProvider.shared = .sdWebImage /// ``` @objc(BRZGIFViewProvider) +@MainActor public final class _OBJC_BRZGIFViewProvider: NSObject { /// The GIF view provider used for all BrazeUI components. @@ -31,7 +32,7 @@ public final class _OBJC_BRZGIFViewProvider: NSObject { /// - Parameters: /// - url: The local file url for the image. @objc - public var view: (_ url: URL?) -> UIView { + public var view: @MainActor @Sendable (_ url: URL?) -> UIView { get { gifViewProvider.view } set { gifViewProvider.view = newValue } } @@ -41,7 +42,7 @@ public final class _OBJC_BRZGIFViewProvider: NSObject { /// - view: The view to update. /// - url: The local file url for the image. @objc - public var updateView: (_ view: UIView, _ url: URL?) -> Void { + public var updateView: @MainActor @Sendable (_ view: UIView, _ url: URL?) -> Void { get { gifViewProvider.updateView } set { gifViewProvider.updateView = newValue } } @@ -54,8 +55,8 @@ public final class _OBJC_BRZGIFViewProvider: NSObject { /// - updateView: See ``updateView``. @objc public convenience init( - view: @escaping (_ url: URL?) -> UIView, - updateView: @escaping (_ view: UIView, _ url: URL?) -> Void + view: @MainActor @Sendable @escaping (_ url: URL?) -> UIView, + updateView: @MainActor @Sendable @escaping (_ view: UIView, _ url: URL?) -> Void ) { self.init(.init(view: view, updateView: updateView)) } @@ -70,8 +71,8 @@ public final class _OBJC_BRZGIFViewProvider: NSObject { /// - updateView: See ``updateView``. @objc public static func provider( - view: @escaping (_ url: URL?) -> UIView, - updateView: @escaping (_ view: UIView, _ url: URL?) -> Void + view: @MainActor @Sendable @escaping (_ url: URL?) -> UIView, + updateView: @MainActor @Sendable @escaping (_ view: UIView, _ url: URL?) -> Void ) -> _OBJC_BRZGIFViewProvider { .init(view: view, updateView: updateView) } diff --git a/Sources/BrazeUI/Dependencies/GIFViewProvider.swift b/Sources/BrazeUI/Dependencies/GIFViewProvider.swift index 4166b9bf4c..ae6a04be9b 100644 --- a/Sources/BrazeUI/Dependencies/GIFViewProvider.swift +++ b/Sources/BrazeUI/Dependencies/GIFViewProvider.swift @@ -14,32 +14,43 @@ import UIKit /// ```swift /// GIFViewProvider.shared = .sdWebImage /// ``` -public struct GIFViewProvider { +public struct GIFViewProvider: Sendable { + + /// The lock guarding the static properties. + private static let lock = NSRecursiveLock() /// The GIF view provider used for all BrazeUI components. /// /// By default, Braze displays animated GIFs as static images. /// See ``GIFViewProvider-swift.struct`` for details about how to add support for animated GIFs. - public static var shared: GIFViewProvider = .nonAnimating + public static var shared: GIFViewProvider { + get { lock.sync { _shared } } + set { lock.sync { _shared = newValue } } + } + #if compiler(>=5.10) + nonisolated(unsafe) private static var _shared: GIFViewProvider = .nonAnimating + #else + private static var _shared: GIFViewProvider = .nonAnimating + #endif /// Creates a view able to display static and animated GIF images. /// - Parameters: /// - url: The local file url for the image. - public var view: (_ url: URL?) -> UIView + public var view: @MainActor @Sendable (_ url: URL?) -> UIView /// Updates the passed view with a new image at `url`. /// - Parameters: /// - view: The view to update. /// - url: The local file url for the image. - public var updateView: (_ view: UIView, _ url: URL?) -> Void + public var updateView: @MainActor @Sendable (_ view: UIView, _ url: URL?) -> Void /// Creates a GIF view provider. /// - Parameters: /// - view: See ``view``. /// - updateView: See ``updateView``. public init( - view: @escaping (URL?) -> UIView, - updateView: @escaping (UIView, URL?) -> Void + view: @MainActor @Sendable @escaping (URL?) -> UIView, + updateView: @MainActor @Sendable @escaping (UIView, URL?) -> Void ) { self.view = view self.updateView = updateView diff --git a/Sources/BrazeUI/Dependencies/KeyboardFrameNotifier.swift b/Sources/BrazeUI/Dependencies/KeyboardFrameNotifier.swift index add37e5aff..646d244a61 100644 --- a/Sources/BrazeUI/Dependencies/KeyboardFrameNotifier.swift +++ b/Sources/BrazeUI/Dependencies/KeyboardFrameNotifier.swift @@ -8,6 +8,7 @@ import UIKit /// `UIKit` does not offer any api to retrieve the current state of the software keyboard, only /// updates. The keyboard frame is `.zero` until the keyboard frame notifier receives an update from /// `UIKit`. +@MainActor open class KeyboardFrameNotifier { /// The shared keyboard frame notifier. diff --git a/Sources/BrazeUI/Dependencies/Shadow.swift b/Sources/BrazeUI/Dependencies/Shadow.swift index 0d70f84378..db2095d0ff 100644 --- a/Sources/BrazeUI/Dependencies/Shadow.swift +++ b/Sources/BrazeUI/Dependencies/Shadow.swift @@ -1,7 +1,7 @@ import UIKit /// Type representing a view's shadow. -public struct Shadow: Equatable { +public struct Shadow: Equatable, Sendable { public var color: UIColor public var offset: CGSize public var radius: CGFloat diff --git a/Sources/BrazeUI/Dependencies/ViewDimension.swift b/Sources/BrazeUI/Dependencies/ViewDimension.swift index e77183f49c..c4c42cc901 100644 --- a/Sources/BrazeUI/Dependencies/ViewDimension.swift +++ b/Sources/BrazeUI/Dependencies/ViewDimension.swift @@ -4,7 +4,7 @@ import UIKit /// /// Large interface values are used when the UI is being displayed with both horizontal and vertical /// size classes are `.regular` and the device is in landscape. -public struct ViewDimension: Hashable { +public struct ViewDimension: Hashable, Sendable { /// The dimension for the regular case. public var regular: Double diff --git a/Sources/BrazeUI/Dependencies/VisibilityTracker.swift b/Sources/BrazeUI/Dependencies/VisibilityTracker.swift index d2720fb5d2..993e90e8fa 100644 --- a/Sources/BrazeUI/Dependencies/VisibilityTracker.swift +++ b/Sources/BrazeUI/Dependencies/VisibilityTracker.swift @@ -2,14 +2,15 @@ import UIKit /// VisibilityTracker keeps track of a list of visible identifiers and can report when they remain /// visible for more than a specified time interval. +@MainActor open class VisibilityTracker { // MARK: - Properties private let interval: TimeInterval - private let visibleIdentifiers: () -> [Identifier] - private let visibleForInterval: (Identifier) -> Void - private let exitVisible: (Identifier, _ afterInterval: Bool) -> Void + private let visibleIdentifiers: @MainActor () -> [Identifier] + private let visibleForInterval: @MainActor (Identifier) -> Void + private let exitVisible: @MainActor (Identifier, _ afterInterval: Bool) -> Void private lazy var displayLink: CADisplayLink = createDisplayLink() private var identifiersMap: [Identifier: CFTimeInterval] = [:] @@ -30,9 +31,9 @@ open class VisibilityTracker { /// than `interval` seconds before its exit. public init( interval: TimeInterval = 0.3, - visibleIdentifiers: @escaping () -> [Identifier], - visibleForInterval: @escaping (Identifier) -> Void = { _ in }, - exitVisible: @escaping (Identifier, _ afterInterval: Bool) -> Void = { _, _ in } + visibleIdentifiers: @MainActor @escaping () -> [Identifier], + visibleForInterval: @MainActor @escaping (Identifier) -> Void = { _ in }, + exitVisible: @MainActor @escaping (Identifier, _ afterInterval: Bool) -> Void = { _, _ in } ) { self.interval = interval self.visibleIdentifiers = visibleIdentifiers diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift index d37b37ebf5..7af568c275 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift @@ -10,6 +10,7 @@ import UIKit /// To add GIF support to the in-app message UI components, set a valid /// ``gifViewProvider-swift.var``. @objc(BrazeInAppMessageUI) +@MainActor open class BrazeInAppMessageUI: NSObject, BrazeInAppMessagePresenter, @@ -245,9 +246,12 @@ open class BrazeInAppMessageUI: if case .auto(let interval) = message.messageClose { dismissTimer?.invalidate() dismissTimer = .scheduledTimer( - withTimeInterval: interval, + timeInterval: interval, + target: self, + selector: #selector(dismissAfterTimer), + userInfo: nil, repeats: false - ) { [weak self] _ in self?.dismiss() } + ) } // Display @@ -284,6 +288,11 @@ open class BrazeInAppMessageUI: window?.messageViewController?.dismissMessage(completion: completion) ?? completion?() } + @objc + private func dismissAfterTimer() { + dismiss(completion: nil) + } + // MARK: - Utils enum PendingDestination { diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate+ObjC.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate+ObjC.swift index 69f193b2fa..52206e1765 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate+ObjC.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate+ObjC.swift @@ -25,6 +25,7 @@ public protocol _OBJC_BrazeInAppMessageUIDelegate: AnyObject { /// - Returns: The display choice for `message`. See ``BrazeInAppMessageUI/DisplayChoice`` for /// possible values. @objc(inAppMessage:displayChoiceForMessage:) + @MainActor optional func _objc_inAppMessage( _ ui: BrazeInAppMessageUI, displayChoiceForMessage message: Braze.InAppMessageRaw @@ -37,6 +38,7 @@ public protocol _OBJC_BrazeInAppMessageUIDelegate: AnyObject { /// - message: The message to be presented. /// - view: The in-app message view. @objc(inAppMessage:willPresent:view:) + @MainActor optional func _objc_inAppMessage( _ ui: BrazeInAppMessageUI, willPresent message: Braze.InAppMessageRaw, @@ -49,6 +51,7 @@ public protocol _OBJC_BrazeInAppMessageUIDelegate: AnyObject { /// - message: The message to be presented. /// - view: The in-app message view. @objc(inAppMessage:didPresent:view:) + @MainActor optional func _objc_inAppMessage( _ ui: BrazeInAppMessageUI, didPresent message: Braze.InAppMessageRaw, @@ -61,6 +64,7 @@ public protocol _OBJC_BrazeInAppMessageUIDelegate: AnyObject { /// - message: The message to be presented. /// - view: The in-app message view. @objc(inAppMessage:willDismiss:view:) + @MainActor optional func _objc_inAppMessage( _ ui: BrazeInAppMessageUI, willDismiss message: Braze.InAppMessageRaw, @@ -74,6 +78,7 @@ public protocol _OBJC_BrazeInAppMessageUIDelegate: AnyObject { /// - message: The message to be presented. /// - view: The in-app message view. @objc(inAppMessage:didDismiss:view:) + @MainActor optional func _objc_inAppMessage( _ ui: BrazeInAppMessageUI, didDismiss message: Braze.InAppMessageRaw, @@ -99,6 +104,7 @@ public protocol _OBJC_BrazeInAppMessageUIDelegate: AnyObject { /// - view: The in-app message view. /// - Returns: `true` to let Braze process the click action, `false` otherwise. @objc(inAppMessage:shouldProcess:url:buttonId:message:view:) + @MainActor optional func _objc_inAppMessage( _ ui: BrazeInAppMessageUI, shouldProcess clickAction: Braze.InAppMessageRaw._OBJC_BRZInAppMessageRawClickAction, @@ -116,6 +122,7 @@ public protocol _OBJC_BrazeInAppMessageUIDelegate: AnyObject { /// - context: The presentation context. See ``BrazeInAppMessageUI/PresentationContextRaw`` for a /// list of supported customizations. @objc(inAppMessage:prepareWith:) + @MainActor optional func _objc_inAppMessage( _ ui: BrazeInAppMessageUI, prepareWith context: BrazeInAppMessageUI.PresentationContextRaw @@ -127,7 +134,7 @@ public protocol _OBJC_BrazeInAppMessageUIDelegate: AnyObject { /// /// See ``BrazeInAppMessageUIDelegate/inAppMessage(_:displayChoiceForMessage:)-1ghly``. @objc(BRZInAppMessageUIDisplayChoice) -public enum _OBJC_BRZInAppMessageUIDisplayChoice: Int { +public enum _OBJC_BRZInAppMessageUIDisplayChoice: Int, Sendable { /// The in-app message is displayed immediately. case now @@ -173,8 +180,13 @@ public enum _OBJC_BRZInAppMessageUIDisplayChoice: Int { /// with the base Swift implementation. final class _OBJC_BrazeInAppMessageUIDelegateWrapper: BrazeInAppMessageUIDelegate { - /// Property used as a unique key for the wrapper lifecycle behavior. - private static var wrapperKey: Void? + // nonisolated(unsafe) attribute for global variable is only available in Xcode 15.3 and later. + #if compiler(>=5.10) + /// Property used as a unique key for the wrapper lifecycle behavior. + nonisolated(unsafe) private static var wrapperKey: Void? + #else + private static var wrapperKey: Void? + #endif /// The ObjC in-app message UI delegate. weak var delegate: _OBJC_BrazeInAppMessageUIDelegate? diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift index dfb67da3ef..6c75ef135e 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift @@ -22,6 +22,7 @@ public protocol BrazeInAppMessageUIDelegate: AnyObject { /// - message: The message to be presented. /// - Returns: The display choice for `message`. See ``BrazeInAppMessageUI/DisplayChoice`` for /// possible values. + @MainActor func inAppMessage( _ ui: BrazeInAppMessageUI, displayChoiceForMessage message: Braze.InAppMessage @@ -34,6 +35,7 @@ public protocol BrazeInAppMessageUIDelegate: AnyObject { /// - ui: The in-app message ui instance. /// - context: The presentation context. See ``BrazeInAppMessageUI/PresentationContext`` for a /// list of supported customizations. + @MainActor func inAppMessage( _ ui: BrazeInAppMessageUI, prepareWith context: inout BrazeInAppMessageUI.PresentationContext @@ -45,6 +47,7 @@ public protocol BrazeInAppMessageUIDelegate: AnyObject { /// - ui: The in-app message ui instance. /// - message: The message to be presented. /// - view: The in-app message view. + @MainActor func inAppMessage( _ ui: BrazeInAppMessageUI, willPresent message: Braze.InAppMessage, @@ -56,6 +59,7 @@ public protocol BrazeInAppMessageUIDelegate: AnyObject { /// - ui: The in-app message ui instance. /// - message: The message to be presented. /// - view: The in-app message view. + @MainActor func inAppMessage( _ ui: BrazeInAppMessageUI, didPresent message: Braze.InAppMessage, @@ -67,6 +71,7 @@ public protocol BrazeInAppMessageUIDelegate: AnyObject { /// - ui: The in-app message ui instance. /// - message: The message to be presented. /// - view: The in-app message view. + @MainActor func inAppMessage( _ ui: BrazeInAppMessageUI, willDismiss message: Braze.InAppMessage, @@ -79,6 +84,7 @@ public protocol BrazeInAppMessageUIDelegate: AnyObject { /// - ui: The in-app message ui instance. /// - message: The message to be presented. /// - view: The in-app message view. + @MainActor func inAppMessage( _ ui: BrazeInAppMessageUI, didDismiss message: Braze.InAppMessage, @@ -102,6 +108,7 @@ public protocol BrazeInAppMessageUIDelegate: AnyObject { /// - message: The message to be presented. /// - view: The in-app message view. /// - Returns: `true` to let Braze process the click action, `false` otherwise. + @MainActor func inAppMessage( _ ui: BrazeInAppMessageUI, shouldProcess clickAction: Braze.InAppMessage.ClickAction, diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIExt.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIExt.swift index f819c21676..d19164779b 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIExt.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIExt.swift @@ -13,7 +13,7 @@ extension BrazeInAppMessageUI { /// The different display choices supported when receiving an in-app message from the Braze SDK. /// /// See ``BrazeInAppMessageUIDelegate/inAppMessage(_:displayChoiceForMessage:)-9w1nb``. - public enum DisplayChoice { + public enum DisplayChoice: Sendable { /// The in-app message is displayed immediately. case now @@ -100,7 +100,7 @@ extension BrazeInAppMessageUI { /// /// InAppMessage types are represented as structs and imported from BrazeKit. /// This wrapper acts as a workaround to prevent Objective-C metaclass errors. - class MessageWrapper { + final class MessageWrapper { var wrappedValue: WrappedType /// Initializes the wrapper with the wrapped struct. diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContext.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContext.swift index f315fb9272..ede26e1432 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContext.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContext.swift @@ -7,6 +7,7 @@ extension BrazeInAppMessageUI { /// /// A writable instance of this type is passed to the ``BrazeInAppMessageUI/delegate`` via the /// ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog`` method. + @MainActor public struct PresentationContext { /// The message to be presented. diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContextRaw.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContextRaw.swift index 8958979934..eb2ba7fa47 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContextRaw.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContextRaw.swift @@ -12,7 +12,8 @@ extension BrazeInAppMessageUI { /// A writable instance of this type is passed to the ``BrazeInAppMessageUI/delegate`` via the /// ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog`` method. @objc(BrazeInAppMessageUIPresentationContextRaw) - public class PresentationContextRaw: NSObject { + @MainActor + public final class PresentationContextRaw: NSObject { /// The message to be presented. /// diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIViewController.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIViewController.swift index e9b7cbeb98..8b32a87173 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIViewController.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIViewController.swift @@ -81,8 +81,9 @@ extension BrazeInAppMessageUI { ui: BrazeInAppMessageUI, context: PresentationContext, messageView: InAppMessageView, - keyboard: KeyboardFrameNotifier = .shared + keyboard: KeyboardFrameNotifier? = nil ) { + let keyboard = keyboard ?? .shared let traits = context.preferencesProxy?.traitCollection self.ui = ui diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageViewAttributes.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageViewAttributes.swift index a46f8de2bc..bc1d050b5d 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageViewAttributes.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageViewAttributes.swift @@ -2,7 +2,7 @@ import BrazeKit extension BrazeInAppMessageUI { - public enum ViewAttributes { + public enum ViewAttributes: Sendable { case slideup(SlideupView.Attributes) case modal(ModalView.Attributes) case modalImage(ModalImageView.Attributes) diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIContainerView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIContainerView.swift index 47c80025c9..c77788e4b1 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIContainerView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIContainerView.swift @@ -12,10 +12,10 @@ extension BrazeInAppMessageUI { /// Creates and returns a container view initialized with a keyboard frame notifier. /// - Parameter keyboard: The keyboard frame notifier. - public init(keyboard: KeyboardFrameNotifier = .shared) { - self.keyboard = keyboard + public init(keyboard: KeyboardFrameNotifier? = nil) { + self.keyboard = keyboard ?? .shared super.init(frame: .zero) - keyboard.subscribe(identifier: ObjectIdentifier(self)) { [weak self] _ in + self.keyboard.subscribe(identifier: ObjectIdentifier(self)) { [weak self] _ in self?.updateConstraintsForKeyboard() } } @@ -25,7 +25,9 @@ extension BrazeInAppMessageUI { } deinit { - keyboard.unsubscribe(identifier: ObjectIdentifier(self)) + isolatedMainActorDeinit { [id = ObjectIdentifier(self), keyboard] in + keyboard.unsubscribe(identifier: id) + } } // MARK: - Layout diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift index 600487c711..a995b5020b 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift @@ -21,7 +21,7 @@ extension BrazeInAppMessageUI { /// delegate. /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the /// `BrazeInAppMessageUI` instance. - public struct Attributes { + public struct Attributes: Sendable { /// The minimum spacing around the content view (used when displayed as modal). public var margin = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) @@ -72,19 +72,32 @@ extension BrazeInAppMessageUI { public var preferredDisplayMode: DisplayMode? /// Closure allowing further customization, executed when the view is about to be presented. - public var onPresent: ((ModalImageView) -> Void)? + public var onPresent: (@MainActor @Sendable (ModalImageView) -> Void)? /// Closure executed every time the view is laid out. - public var onLayout: ((ModalImageView) -> Void)? + public var onLayout: (@MainActor @Sendable (ModalImageView) -> Void)? /// Closure executed every time the view update its theme. - public var onTheme: ((ModalImageView) -> Void)? + public var onTheme: (@MainActor @Sendable (ModalImageView) -> Void)? /// The defaults full image view attributes. /// /// Modify this value directly to apply the customizations to all full image in-app messages /// presented by the SDK. - public static var defaults = Self() + public static var defaults: Self { + get { lock.sync { _defaults } } + set { lock.sync { _defaults = newValue } } + } + + // nonisolated(unsafe) attribute for global variable is only available in Xcode 15.3 and later. + #if compiler(>=5.10) + nonisolated(unsafe) private static var _defaults = Self() + #else + private static var _defaults = Self() + #endif + + /// The lock guarding the static properties. + private static let lock = NSRecursiveLock() } @@ -93,7 +106,7 @@ extension BrazeInAppMessageUI { } /// Display modes supported by the full image in-app message view. - public enum DisplayMode { + public enum DisplayMode: Sendable { /// Displays the view as a modal. case modal diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift index 28549556f3..512e2cb17f 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift @@ -20,7 +20,7 @@ extension BrazeInAppMessageUI { /// delegate. /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the /// `BrazeInAppMessageUI` instance. - public struct Attributes { + public struct Attributes: Sendable { /// The minimum spacing around the content view (used when displayed as modal). public var margin = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) @@ -79,19 +79,32 @@ extension BrazeInAppMessageUI { public var preferredDisplayMode: DisplayMode? /// Closure allowing further customization, executed when the view is about to be presented. - public var onPresent: ((FullView) -> Void)? + public var onPresent: (@MainActor @Sendable (FullView) -> Void)? /// Closure executed every time the view is laid out. - public var onLayout: ((FullView) -> Void)? + public var onLayout: (@MainActor @Sendable (FullView) -> Void)? /// Closure executed every time the view update its theme. - public var onTheme: ((FullView) -> Void)? + public var onTheme: (@MainActor @Sendable (FullView) -> Void)? /// The defaults full view attributes. /// /// Modify this value directly to apply the customizations to all full in-app messages /// presented by the SDK. - public static var defaults = Self() + public static var defaults: Self { + get { lock.sync { _defaults } } + set { lock.sync { _defaults = newValue } } + } + + // nonisolated(unsafe) attribute for global variable is only available in Xcode 15.3 and later. + #if compiler(>=5.10) + nonisolated(unsafe) private static var _defaults = Self() + #else + private static var _defaults = Self() + #endif + + /// The lock guarding the static properties. + private static let lock = NSRecursiveLock() } @@ -100,7 +113,7 @@ extension BrazeInAppMessageUI { } /// Display modes supported by the full in-app message view. - public enum DisplayMode { + public enum DisplayMode: Sendable { /// Displays the view as a modal. case modal diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIHtmlView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIHtmlView.swift index 0050354819..292f685745 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIHtmlView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIHtmlView.swift @@ -30,7 +30,7 @@ extension BrazeInAppMessageUI { /// delegate. /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the /// `BrazeInAppMessageUI` instance. - public struct Attributes { + public struct Attributes: Sendable { /// The animation used to present the view. public var animation: Animation = .auto @@ -55,22 +55,35 @@ extension BrazeInAppMessageUI { public var allowInspector: Bool = true /// Closure allowing customization of the configuration used by the web view. - public var configure: ((WKWebViewConfiguration) -> Void)? + public var configure: (@MainActor @Sendable (WKWebViewConfiguration) -> Void)? /// Closure allowing further customization, executed when the view is about to be presented. - public var onPresent: ((HtmlView) -> Void)? + public var onPresent: (@MainActor @Sendable (HtmlView) -> Void)? /// Closure executed every time the view is laid out. - public var onLayout: ((HtmlView) -> Void)? + public var onLayout: (@MainActor @Sendable (HtmlView) -> Void)? /// Closure executed every time the view update its theme. - public var onTheme: ((HtmlView) -> Void)? + public var onTheme: (@MainActor @Sendable (HtmlView) -> Void)? /// The defaults html view attributes. /// /// Modify this value directly to apply the customizations to all html in-app messages /// presented by the SDK. - public static var defaults = Self() + public static var defaults: Self { + get { lock.sync { _defaults } } + set { lock.sync { _defaults = newValue } } + } + + // nonisolated(unsafe) attribute for global variable is only available in Xcode 15.3 and later. + #if compiler(>=5.10) + nonisolated(unsafe) private static var _defaults = Self() + #else + private static var _defaults = Self() + #endif + + /// The lock guarding the static properties. + private static let lock = NSRecursiveLock() } /// The view attributes (see ``Attributes-swift.struct``). @@ -79,7 +92,7 @@ extension BrazeInAppMessageUI { // MARK: - Animation /// The presentation animations supported by the html in-app message view. - public enum Animation { + public enum Animation: Sendable { /// The animation chosen is automatically by the view. /// @@ -166,11 +179,13 @@ extension BrazeInAppMessageUI { // - strongly retain its scripts and script message handlers // - seems to outlive the configuration / web view instance // - manual cleanup here ensure proper deallocation of those objects - let userContentController = webView?.configuration.userContentController - userContentController?.removeAllUserScripts() - userContentController?.removeScriptMessageHandler( - forName: Braze.WebViewBridge.ScriptMessageHandler.name - ) + isolatedMainActorDeinit { [webView] in + let userContentController = webView?.configuration.userContentController + userContentController?.removeAllUserScripts() + userContentController?.removeScriptMessageHandler( + forName: Braze.WebViewBridge.ScriptMessageHandler.name + ) + } } // MARK: - Theme diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift index b375c8bed8..3a5419a97d 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift @@ -29,7 +29,7 @@ extension BrazeInAppMessageUI { /// delegate. /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the /// `BrazeInAppMessageUI` instance. - public struct Attributes { + public struct Attributes: Sendable { /// The minimum spacing around the content view. public var margin = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) @@ -76,19 +76,32 @@ extension BrazeInAppMessageUI { public var buttonsAttributes = ButtonView.Attributes.defaults /// Closure allowing further customization, executed when the view is about to be presented. - public var onPresent: ((ModalImageView) -> Void)? + public var onPresent: (@MainActor @Sendable (ModalImageView) -> Void)? /// Closure executed every time the view is laid out. - public var onLayout: ((ModalImageView) -> Void)? + public var onLayout: (@MainActor @Sendable (ModalImageView) -> Void)? /// Closure executed every time the view update its theme. - public var onTheme: ((ModalImageView) -> Void)? + public var onTheme: (@MainActor @Sendable (ModalImageView) -> Void)? /// The defaults modal image view attributes. /// /// Modify this value directly to apply the customizations to all modal in-app messages /// presented by the SDK. - public static var defaults = Self() + public static var defaults: Self { + get { lock.sync { _defaults } } + set { lock.sync { _defaults = newValue } } + } + + // nonisolated(unsafe) attribute for global variable is only available in Xcode 15.3 and later. + #if compiler(>=5.10) + nonisolated(unsafe) private static var _defaults = Self() + #else + private static var _defaults = Self() + #endif + + /// The lock guarding the static properties. + private static let lock = NSRecursiveLock() } diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift index 93f51665f6..a9f0989628 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift @@ -40,7 +40,7 @@ extension BrazeInAppMessageUI { /// delegate. /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the /// `BrazeInAppMessageUI` instance. - public struct Attributes { + public struct Attributes: Sendable { /// The minimum spacing around the content view. public var margin: UIEdgeInsets = .init(top: 20, left: 20, bottom: 20, right: 20) @@ -95,19 +95,32 @@ extension BrazeInAppMessageUI { public var buttonsAttributes: BrazeInAppMessageUI.ButtonView.Attributes = .defaults /// Closure allowing further customization, executed when the view is about to be presented. - public var onPresent: ((ModalView) -> Void)? + public var onPresent: (@MainActor @Sendable (ModalView) -> Void)? /// Closure executed every time the view is laid out. - public var onLayout: ((ModalView) -> Void)? + public var onLayout: (@MainActor @Sendable (ModalView) -> Void)? /// Closure executed every time the view update its theme. - public var onTheme: ((ModalView) -> Void)? + public var onTheme: (@MainActor @Sendable (ModalView) -> Void)? /// The defaults modal view attributes. /// /// Modify this value directly to apply the customizations to all modal in-app messages /// presented by the SDK. - public static var defaults = Self() + public static var defaults: Self { + get { lock.sync { _defaults } } + set { lock.sync { _defaults = newValue } } + } + + // nonisolated(unsafe) attribute for global variable is only available in Xcode 15.3 and later. + #if compiler(>=5.10) + nonisolated(unsafe) private static var _defaults = Self() + #else + private static var _defaults = Self() + #endif + + /// The lock guarding the static properties. + private static let lock = NSRecursiveLock() } public var attributes: Attributes { diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift index 6a91d12005..e2cbaeb023 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift @@ -29,7 +29,7 @@ extension BrazeInAppMessageUI { /// delegate. /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the /// `BrazeInAppMessageUI` instance. - public struct Attributes { + public struct Attributes: Sendable { /// The minimum spacing around the content view. public var margin = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) @@ -87,24 +87,37 @@ extension BrazeInAppMessageUI { public var maxWidth = 450.0 /// Closure allowing further customization, executed when the view is about to be presented. - public var onPresent: ((SlideupView) -> Void)? + public var onPresent: (@MainActor @Sendable (SlideupView) -> Void)? /// Closure executed every time the view is laid out. - public var onLayout: ((SlideupView) -> Void)? + public var onLayout: (@MainActor @Sendable (SlideupView) -> Void)? /// Closure executed every time the view update its theme. - public var onTheme: ((SlideupView) -> Void)? + public var onTheme: (@MainActor @Sendable (SlideupView) -> Void)? /// The defaults slideup view attributes. /// /// Modify this value directly to apply the customizations to all slideup in-app messages /// presented by the SDK. - public static var defaults = Self() + public static var defaults: Self { + get { lock.sync { _defaults } } + set { lock.sync { _defaults = newValue } } + } + + // nonisolated(unsafe) attribute for global variable is only available in Xcode 15.3 and later. + #if compiler(>=5.10) + nonisolated(unsafe) private static var _defaults = Self() + #else + private static var _defaults = Self() + #endif + + /// The lock guarding the static properties. + private static let lock = NSRecursiveLock() } /// Visibility options supported for the chevron. - public enum ChevronVisibility { + public enum ChevronVisibility: Sendable { /// Visible when the message has a click action, hidden otherwise. case auto @@ -477,7 +490,8 @@ extension BrazeInAppMessageUI { contentView.point(inside: convert(point, to: contentView), with: event) } - class PressGestureDelegate: NSObject, UIGestureRecognizerDelegate { + @MainActor + final class PressGestureDelegate: NSObject, UIGestureRecognizerDelegate { func gestureRecognizer( _ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageView.swift index 580946fffa..176756e349 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageView.swift @@ -13,6 +13,7 @@ import UIKit /// - ``BrazeInAppMessageUI/ControlView`` /// /// Custom in-app message views must conform to this protocol. +@MainActor public protocol InAppMessageView: UIView { /// The current presented state — is the message view visible to the user. diff --git a/Sources/BrazeUI/InAppMessageUI/Views/Misc/ButtonView.swift b/Sources/BrazeUI/InAppMessageUI/Views/Misc/ButtonView.swift index c3e93ee9ff..66e6449968 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/Misc/ButtonView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/Misc/ButtonView.swift @@ -69,7 +69,7 @@ extension BrazeInAppMessageUI { // MARK: - Attributes /// Attributes allow a high-level customization of Braze's UI component. - public struct Attributes { + public struct Attributes: Sendable { /// Space around the button label, inside the button border. /// @@ -106,7 +106,20 @@ extension BrazeInAppMessageUI { /// /// Modify this value directly to apply the customizations to all in-app message buttons /// presented by the SDK. - public static var defaults = Self() + public static var defaults: Self { + get { lock.sync { _defaults } } + set { lock.sync { _defaults = newValue } } + } + + // nonisolated(unsafe) attribute for global variable is only available in Xcode 15.3 and later. + #if compiler(>=5.10) + nonisolated(unsafe) private static var _defaults = Self() + #else + private static var _defaults = Self() + #endif + + /// The lock guarding the static properties. + private static let lock = NSRecursiveLock() } /// The high-level customization struct. diff --git a/Sources/BrazeUI/InAppMessageUI/Views/Misc/IconView.swift b/Sources/BrazeUI/InAppMessageUI/Views/Misc/IconView.swift index 15b9ac64a8..1e6e4d45f5 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/Misc/IconView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/Misc/IconView.swift @@ -54,7 +54,7 @@ extension BrazeInAppMessageUI { // MARK: - Attributes /// Attributes allow high-level customization of Braze's UI component. - public struct Attributes { + public struct Attributes: Sendable { /// Intrinsic size of the icon, including the background. /// diff --git a/Sources/BrazeUI/InAppMessageUI/Views/Misc/ModalTextView.swift b/Sources/BrazeUI/InAppMessageUI/Views/Misc/ModalTextView.swift index 0194c6148d..585217a419 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/Misc/ModalTextView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/Misc/ModalTextView.swift @@ -5,7 +5,7 @@ extension BrazeInAppMessageUI { // A view displaying modal and full in-app messages' header and message according the Braze's // specs. - public class ModalTextView: UIScrollView { + public final class ModalTextView: UIScrollView { /// The view's header string. public var header: String { @@ -80,7 +80,7 @@ extension BrazeInAppMessageUI { // MARK: - Attributes /// The attributes supported by the modal text view. - public struct Attributes { + public struct Attributes: Sendable { /// The style for the header. public var header = TextStyle() @@ -92,7 +92,7 @@ extension BrazeInAppMessageUI { } /// The style for displaying text in the modal text view. - public struct TextStyle { + public struct TextStyle: Sendable { /// The text color. public var color: UIColor = .brazeLabel diff --git a/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView+ButtonViews.swift b/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView+ButtonViews.swift index 345cc52337..79c6083876 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView+ButtonViews.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView+ButtonViews.swift @@ -22,7 +22,7 @@ extension BrazeInAppMessageUI.StackView { let subviews: [UIView] = buttons .map { data in - let button = BrazeInAppMessageUI.ButtonView(button: data) + let button = BrazeInAppMessageUI.ButtonView(button: data, attributes: attributes) button.addAction { onClick(data) } return button } diff --git a/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView.swift b/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView.swift index dc95253428..7750523673 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView.swift @@ -7,7 +7,7 @@ extension BrazeInAppMessageUI { /// Before iOS 14, `UIStackView` uses a non-rendering `CATransformLayer` instead of a classic /// `CALayer` (see [tweet](https://archive.md/t0AIh)). This wrapper view allow to set the layer's /// properties on pre-iOS 14 devices - public class StackView: UIView { + public final class StackView: UIView { /// The inner stack view. public let stack = UIStackView() @@ -29,7 +29,7 @@ extension BrazeInAppMessageUI { /// See `UIStackView/init(arrangedSubviews:)`. convenience init(arrangedSubviews subviews: [UIView]) { self.init(frame: .zero) - subviews.forEach(stack.addArrangedSubview) + subviews.forEach { stack.addArrangedSubview($0) } } required init(coder: NSCoder) { diff --git a/Sources/BrazeUI/Resources.swift b/Sources/BrazeUI/Resources.swift index 98b6b4c0c7..179c20b7a3 100644 --- a/Sources/BrazeUI/Resources.swift +++ b/Sources/BrazeUI/Resources.swift @@ -1,18 +1,30 @@ import Foundation +/// Lock guarding ``BrazeUI/overrideResourcesBundle``. +private let lock = NSRecursiveLock() + /// The override bundle BrazeUI uses to load resources (default: `nil`). /// /// Set this property to the bundle containing BrazeUI resources when your project cannot /// automatically include the resources (e.g. Tuist setup) /// /// - Important: This property needs to be set before the SDK initialization. -public var overrideResourcesBundle: Bundle? +public var overrideResourcesBundle: Bundle? { + get { lock.sync { _overrideResourcesBundle } } + set { lock.sync { _overrideResourcesBundle = newValue } } +} +// nonisolated(unsafe) attribute for global variable is only available in Xcode 15.3 and later. +#if compiler(>=5.10) + nonisolated(unsafe) private var _overrideResourcesBundle: Bundle? +#else + private var _overrideResourcesBundle: Bundle? +#endif private class BundleFinder {} /// Resources related utilities. @objc(BRZUIResources) -public class BrazeUIResources: NSObject { +public final class BrazeUIResources: NSObject { /// The resources bundle. @objc