From 2e1a9e2bb50b145c13e759865fc4fce3c646a613 Mon Sep 17 00:00:00 2001 From: Louis Bur Date: Fri, 19 Aug 2022 11:59:13 -0400 Subject: [PATCH] Version 5.2.0 --- BrazeKit.podspec | 6 +- BrazeLocation.podspec | 8 +- BrazeNotificationService.podspec | 6 +- BrazePushStory.podspec | 6 +- BrazeUI.podspec | 8 +- CHANGELOG.md | 11 + .../project.pbxproj | 105 +++ .../contents.xcworkspacedata | 10 + .../project.pbxproj | 137 ++++ .../xcshareddata/swiftpm/Package.resolved | 78 ++- Examples/Podfile | 13 +- Examples/Sources/Analytics/AppDelegate.swift | 2 +- .../Analytics/AuthenticationManager.swift | 2 +- Examples/Sources/Analytics/Readme.swift | 2 +- .../Sources/Common/ReadmeViewController.swift | 20 +- .../Sources/ContentCards/AppDelegate.swift | 94 +++ Examples/Sources/ContentCards/Readme.swift | 46 ++ .../SDWebImageGIFViewProvider.swift | 20 + .../Sources/InAppMessages/AppDelegate.swift | 7 +- Examples/Sources/InAppMessages/Readme.swift | 7 +- .../SDWebImageGIFViewProvider.swift | 6 +- Examples/Sources/Location/AppDelegate.swift | 2 +- Examples/Sources/Location/Readme.swift | 20 +- Examples/Sources/ObjC/Info.plist | 26 + .../PushNotifications/AppDelegate.swift | 24 +- .../Sources/PushNotifications/Readme.swift | 3 +- Package.swift | 16 +- README.md | 1 - Sources/BrazeUI/BrazeUIMocks.swift | 10 +- .../Cells/ContentCardUIBannerCell.swift | 83 +++ .../ContentCardUICaptionedImageCell.swift | 119 ++++ .../Cells/ContentCardUICell.swift | 381 ++++++++++ .../Cells/ContentCardUIClassicCell.swift | 97 +++ .../Cells/ContentCardUIClassicImageCell.swift | 129 ++++ .../Cells/ContentCardUIControlCell.swift | 26 + .../Cells/ContentCardUIImageCell.swift | 18 + .../Cells/ContentCardUITextStack.swift | 72 ++ .../ContentCardUI/ContentCardMocks.swift | 267 +++++++ .../BrazeUI/ContentCardUI/ContentCardUI.swift | 28 + .../ContentCardUIModalViewController.swift | 87 +++ .../ContentCardUI/ContentCardUISwiftUI.swift | 94 +++ .../ContentCardUIViewController.swift | 659 ++++++++++++++++++ .../ContentCardUIViewControllerDelegate.swift | 41 ++ .../BrazeUI/Dependencies/AsyncImageView.swift | 165 +++++ .../GIFViewProvider.swift | 18 +- Sources/BrazeUI/Dependencies/Localize.swift | 5 +- Sources/BrazeUI/Dependencies/Shadow.swift | 67 +- Sources/BrazeUI/Dependencies/ShadowView.swift | 74 ++ Sources/BrazeUI/Dependencies/UIKitExt.swift | 99 +++ .../Dependencies/VisibilityTracker.swift | 113 +++ .../InAppMessageUI/InAppMessageMocks.swift | 12 + .../InAppMessageUI/InAppMessageUI.swift | 3 +- .../InAppMessageUIDelegate.swift | 2 +- .../Views/InAppMessageUIFullImageView.swift | 2 +- .../Views/InAppMessageUIFullView.swift | 2 +- .../Views/InAppMessageUIModalImageView.swift | 12 +- .../Views/InAppMessageUIModalView.swift | 10 +- .../Views/InAppMessageUISlideupView.swift | 39 +- Sources/BrazeUI/PreviewProviders.swift | 27 + .../Assets.xcassets/ContentCard/Contents.json | 9 + .../ContentCard/pin.imageset/Contents.json | 23 + .../ContentCard/pin.imageset/pin.png | Bin 0 -> 204 bytes .../ContentCard/pin.imageset/pin@2x.png | Bin 0 -> 289 bytes .../ContentCard/pin.imageset/pin@3x.png | Bin 0 -> 399 bytes .../ContentCard/retry.imageset/Contents.json | 23 + .../ContentCard/retry.imageset/retry.png | Bin 0 -> 2078 bytes .../ContentCard/retry.imageset/retry@2x.png | Bin 0 -> 4584 bytes .../ContentCard/retry.imageset/retry@3x.png | Bin 0 -> 7406 bytes .../ContentCardsLocalizable.strings | 1 + .../ar.lproj/ContentCardsLocalizable.strings | 1 + .../cs.lproj/ContentCardsLocalizable.strings | 1 + .../da.lproj/ContentCardsLocalizable.strings | 1 + .../de.lproj/ContentCardsLocalizable.strings | 1 + .../en.lproj/ContentCardsLocalizable.strings | 1 + .../ContentCardsLocalizable.strings | 1 + .../ContentCardsLocalizable.strings | 1 + .../es.lproj/ContentCardsLocalizable.strings | 1 + .../et.lproj/ContentCardsLocalizable.strings | 1 + .../fi.lproj/ContentCardsLocalizable.strings | 1 + .../fil.lproj/ContentCardsLocalizable.strings | 1 + .../fr.lproj/ContentCardsLocalizable.strings | 1 + .../he.lproj/ContentCardsLocalizable.strings | 1 + .../hi.lproj/ContentCardsLocalizable.strings | 1 + .../id.lproj/ContentCardsLocalizable.strings | 1 + .../it.lproj/ContentCardsLocalizable.strings | 1 + .../ja.lproj/ContentCardsLocalizable.strings | 1 + .../km.lproj/ContentCardsLocalizable.strings | 1 + .../ko.lproj/ContentCardsLocalizable.strings | 1 + .../lo.lproj/ContentCardsLocalizable.strings | 1 + .../ms.lproj/ContentCardsLocalizable.strings | 1 + .../my.lproj/ContentCardsLocalizable.strings | 1 + .../nb.lproj/ContentCardsLocalizable.strings | 1 + .../nl.lproj/ContentCardsLocalizable.strings | 1 + .../pl.lproj/ContentCardsLocalizable.strings | 1 + .../ContentCardsLocalizable.strings | 1 + .../pt.lproj/ContentCardsLocalizable.strings | 1 + .../ru.lproj/ContentCardsLocalizable.strings | 1 + .../sv.lproj/ContentCardsLocalizable.strings | 1 + .../th.lproj/ContentCardsLocalizable.strings | 1 + .../uk.lproj/ContentCardsLocalizable.strings | 1 + .../vi.lproj/ContentCardsLocalizable.strings | 1 + .../ContentCardsLocalizable.strings | 1 + .../ContentCardsLocalizable.strings | 1 + .../ContentCardsLocalizable.strings | 1 + .../ContentCardsLocalizable.strings | 1 + .../zh.lproj/ContentCardsLocalizable.strings | 1 + 106 files changed, 3366 insertions(+), 174 deletions(-) create mode 100644 Examples/Examples-CocoaPods.xcworkspace/contents.xcworkspacedata create mode 100644 Examples/Sources/ContentCards/AppDelegate.swift create mode 100644 Examples/Sources/ContentCards/Readme.swift create mode 100644 Examples/Sources/ContentCards/SDWebImageGIFViewProvider.swift create mode 100644 Examples/Sources/ObjC/Info.plist create mode 100644 Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIBannerCell.swift create mode 100644 Sources/BrazeUI/ContentCardUI/Cells/ContentCardUICaptionedImageCell.swift create mode 100644 Sources/BrazeUI/ContentCardUI/Cells/ContentCardUICell.swift create mode 100644 Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIClassicCell.swift create mode 100644 Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIClassicImageCell.swift create mode 100644 Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIControlCell.swift create mode 100644 Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIImageCell.swift create mode 100644 Sources/BrazeUI/ContentCardUI/Cells/ContentCardUITextStack.swift create mode 100644 Sources/BrazeUI/ContentCardUI/ContentCardMocks.swift create mode 100644 Sources/BrazeUI/ContentCardUI/ContentCardUI.swift create mode 100644 Sources/BrazeUI/ContentCardUI/ContentCardUIModalViewController.swift create mode 100644 Sources/BrazeUI/ContentCardUI/ContentCardUISwiftUI.swift create mode 100644 Sources/BrazeUI/ContentCardUI/ContentCardUIViewController.swift create mode 100644 Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate.swift create mode 100644 Sources/BrazeUI/Dependencies/AsyncImageView.swift rename Sources/BrazeUI/Dependencies/{GIFViewProvider => }/GIFViewProvider.swift (79%) create mode 100644 Sources/BrazeUI/Dependencies/ShadowView.swift create mode 100644 Sources/BrazeUI/Dependencies/VisibilityTracker.swift create mode 100644 Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/Contents.json create mode 100644 Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/Contents.json create mode 100644 Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/pin.png create mode 100644 Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/pin@2x.png create mode 100644 Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/pin@3x.png create mode 100644 Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/Contents.json create mode 100644 Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/retry.png create mode 100644 Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/retry@2x.png create mode 100644 Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/retry@3x.png create mode 100644 Sources/BrazeUI/Resources/Localization/Base.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/ar.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/cs.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/da.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/de.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/en.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/es-419.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/es-MX.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/es.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/et.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/fi.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/fil.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/fr.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/he.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/hi.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/id.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/it.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/ja.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/km.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/ko.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/lo.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/ms.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/my.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/nb.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/nl.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/pl.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/pt-PT.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/pt.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/ru.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/sv.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/th.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/uk.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/vi.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/zh-HK.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/zh-Hans.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/zh-Hant.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/zh-TW.lproj/ContentCardsLocalizable.strings create mode 100644 Sources/BrazeUI/Resources/Localization/zh.lproj/ContentCardsLocalizable.strings diff --git a/BrazeKit.podspec b/BrazeKit.podspec index 3061ab89d5..e2cc4ee2f9 100644 --- a/BrazeKit.podspec +++ b/BrazeKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeKit' - s.version = '5.1.0' + s.version = '5.2.0' s.summary = 'Braze Main SDK library providing support for analytics and push notifications.' s.homepage = 'https://braze.com' @@ -8,8 +8,8 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.1.0/BrazeKit-CocoaPods.zip', - :sha256 => 'dfedd4d9375d4b71be2f4b4b0e604cd3e931339f92f55a37c90a8500f34a7b9a' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.2.0/BrazeKit-CocoaPods.zip', + :sha256 => '65f46bdb2d365a97e7d5cb7807e460098040fd08013f9b41c868332c92907cb4' } s.swift_version = '5.0' diff --git a/BrazeLocation.podspec b/BrazeLocation.podspec index 9b125f4a96..5a974d521d 100644 --- a/BrazeLocation.podspec +++ b/BrazeLocation.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeLocation' - s.version = '5.1.0' + s.version = '5.2.0' s.summary = 'Braze location library providing support for location analytics and geofence monitoring.' s.homepage = 'https://braze.com' @@ -8,8 +8,8 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.1.0/BrazeLocation-CocoaPods.zip', - :sha256 => '8d1c1b04e586b10dc4987ff57e91ac236246e519fbc214d9b3a2683bdb008b90' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.2.0/BrazeLocation-CocoaPods.zip', + :sha256 => '52b0736d7d1a2313e52ba8ea2f7c6d929c1544e21783ea731bd461979e62ecc5' } s.swift_version = '5.0' @@ -19,5 +19,5 @@ Pod::Spec.new do |s| # Depends on BrazeKit because BrazeKit includes the internal _BrazeLocationClient symbols required # for linking against BrazeLocation. - s.dependency 'BrazeKit', '5.1.0' + s.dependency 'BrazeKit', '5.2.0' end diff --git a/BrazeNotificationService.podspec b/BrazeNotificationService.podspec index f81bf19676..83c3a427e6 100644 --- a/BrazeNotificationService.podspec +++ b/BrazeNotificationService.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeNotificationService' - s.version = '5.1.0' + s.version = '5.2.0' s.summary = 'Braze notification service extension library providing support for Rich Push notifications.' s.homepage = 'https://braze.com' @@ -8,8 +8,8 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.1.0/BrazeNotificationService-CocoaPods.zip', - :sha256 => '43d2b4cb740391029f40b56ad4a71044498dddad56cf185ab9695aeb7ca550f4' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.2.0/BrazeNotificationService-CocoaPods.zip', + :sha256 => '83877056947d5169827334dd1edc459defd9fa3f2734c925f2103ba2a8a77ea5' } s.swift_version = '5.0' diff --git a/BrazePushStory.podspec b/BrazePushStory.podspec index c34b348c8a..3ebc8ee13f 100644 --- a/BrazePushStory.podspec +++ b/BrazePushStory.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazePushStory' - s.version = '5.1.0' + s.version = '5.2.0' s.summary = 'Braze notification content extension library providing support for Push Stories.' s.homepage = 'https://braze.com' @@ -8,8 +8,8 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.1.0/BrazePushStory-CocoaPods.zip', - :sha256 => 'b7478167cf457883c6b7ad4455b3a2acd805f258555a9e55bd427b9eeb933de9' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.2.0/BrazePushStory-CocoaPods.zip', + :sha256 => '7b5e803fad8a2e16b7ad86c4ba47d445fce54efbbac1871e794e52a97e890fa5' } s.swift_version = '5.0' diff --git a/BrazeUI.podspec b/BrazeUI.podspec index 2ad0e6038a..cb4ed4bc98 100644 --- a/BrazeUI.podspec +++ b/BrazeUI.podspec @@ -1,20 +1,20 @@ Pod::Spec.new do |s| s.name = 'BrazeUI' - s.version = '5.1.0' + s.version = '5.2.0' s.summary = 'Braze-provided user interface library for In-App Messages.' s.homepage = 'https://braze.com' s.license = { :type => 'Commercial' } s.authors = 'Braze, Inc.' - s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '5.1.0' } + s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '5.2.0' } s.swift_version = '5.0' - s.ios.deployment_target = '10.0' + s.ios.deployment_target = '11.0' s.static_framework = true s.source_files = 'Sources/BrazeUI/**/*.swift' s.resource_bundles = { 'BrazeUI' => 'Sources/BrazeUI/Resources/**/*' } - s.dependency 'BrazeKit', '5.1.0' + s.dependency 'BrazeKit', '5.2.0' end diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c4ddcb210..9c402951af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 5.2.0 + +##### Added + +- Adds [Content Cards](https://www.braze.com/docs/user_guide/message_building_by_channel/content_cards) support. + - See the [_Content Cards UI_](https://braze-inc.github.io/braze-swift-sdk/tutorials/braze/c2-contentcardsui) tutorial to get started. + +##### Changed + +- Raises `BrazeUI` minimum deployment target to iOS 11.0 to allow providing SwiftUI compatible Views. + ## 5.1.0 ##### Fixed diff --git a/Examples/Examples-CocoaPods.xcodeproj/project.pbxproj b/Examples/Examples-CocoaPods.xcodeproj/project.pbxproj index 5083bc2647..8671ebcaa4 100644 --- a/Examples/Examples-CocoaPods.xcodeproj/project.pbxproj +++ b/Examples/Examples-CocoaPods.xcodeproj/project.pbxproj @@ -9,18 +9,22 @@ /* Begin PBXBuildFile section */ 0DE3276319B3F7EDE9E5761D /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA261A3969E80D4910B919 /* NotificationViewController.swift */; }; 161DC1259C44AD89479BA698 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA064323F91A22F4E14CAE9D /* NotificationService.swift */; }; + 243D978233307D328A4A1EE4 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0078638D97BB480D82B18D6C /* Credentials.swift */; }; 2A65F13CBADEAEA072B1962A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CABCDD10D6E23AB146F9F7BA /* LaunchScreen.storyboard */; }; 30396C24BE39469AE1AFC192 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405EC9F18FB085ECB16DB4F2 /* AppDelegate.swift */; }; + 30D7A569B1A652D533C6DFF4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CABCDD10D6E23AB146F9F7BA /* LaunchScreen.storyboard */; }; 3212C3821215C481B3F4B6C4 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0078638D97BB480D82B18D6C /* Credentials.swift */; }; 40FB9F2DCF0F367CEA6D8E12 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0078638D97BB480D82B18D6C /* Credentials.swift */; }; 5250467C99642CB433B5BE53 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59732946C90C96FDFD43567E /* AppDelegate.swift */; }; 53283BEA84602E64804A8190 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0041AFC6938603AA8E408C85 /* AppDelegate.swift */; }; 56E71FBF34C707A0C76D4E92 /* PushNotificationsServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 40B660242E5E30D22F3AC51E /* PushNotificationsServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 5752C25DC8C2CA0C0925C10F /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6208C00FB5F7F0284A26EF20 /* UserNotifications.framework */; }; + 5B99366856EF616F55DB7F14 /* SDWebImageGIFViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC13846BDD825F17C9C23B9B /* SDWebImageGIFViewProvider.swift */; }; 5D1F19E9A92921557D3ABD06 /* SDWebImageGIFViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF74213486BAEC4E0C94F02D /* SDWebImageGIFViewProvider.swift */; }; 5F9F4D1EEA1EB707C67B5E6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0340E1FFDDBC0C9D72FCD4D4 /* Assets.xcassets */; }; 60D9B416B69C17AD0FF5FD87 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6208C00FB5F7F0284A26EF20 /* UserNotifications.framework */; }; 63632CFE7FB3A8E86B4512BF /* AuthenticationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9A4EACB9451C069A63F43F /* AuthenticationManager.swift */; }; + 7489ACB9F90DED02AA3B64EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03168F8FCB98FF73CC62A64A /* AppDelegate.swift */; }; 7FC56E25CCA1B6080699728A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0340E1FFDDBC0C9D72FCD4D4 /* Assets.xcassets */; }; 88999EE63EC7F6752930AAFA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CABCDD10D6E23AB146F9F7BA /* LaunchScreen.storyboard */; }; 9463BF24AAC12ACDDE2FAC68 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0078638D97BB480D82B18D6C /* Credentials.swift */; }; @@ -30,6 +34,7 @@ AEDDEDC6F9B5D7838BE908D6 /* PushNotificationsContentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7EFD35FD440D538F5EB8F021 /* PushNotificationsContentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B2EED1BA8A1282A0D93EFD26 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0340E1FFDDBC0C9D72FCD4D4 /* Assets.xcassets */; }; B485B344BA32445D3024950D /* CheckoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A11FA051B21C67BB9D89127 /* CheckoutViewController.swift */; }; + C38B9EA3B93DEF499E7C6FDE /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED6428DF0330FE85A4FF7135 /* Readme.swift */; }; C4E0F65CBA014F576019433C /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = C938B015CECD020B6FA2206F /* Readme.swift */; }; C50931E7BC0D74EA19E0F507 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76740E64DEB7874C6DA73841 /* ReadmeViewController.swift */; }; C8D253B0D10AD3214F6AF79D /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76740E64DEB7874C6DA73841 /* ReadmeViewController.swift */; }; @@ -38,7 +43,9 @@ E5D45B49FBA2F4F11D9DBBAD /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76740E64DEB7874C6DA73841 /* ReadmeViewController.swift */; }; E63FFCC8439CC53E8F297431 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1874579DE7E406E73A3CB3A1 /* UserNotificationsUI.framework */; }; F42CC1B49E69F3E73EFAAA3C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514DAF551BD9AE51D34F1ACB /* AppDelegate.swift */; }; + F49C339077170329AC8926A3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0340E1FFDDBC0C9D72FCD4D4 /* Assets.xcassets */; }; F5EEEAF59925C09D6B71D2AE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CABCDD10D6E23AB146F9F7BA /* LaunchScreen.storyboard */; }; + F7BCA95A9B52DF8721CEC8EF /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76740E64DEB7874C6DA73841 /* ReadmeViewController.swift */; }; F8A17ADE416B1BDA0913DFC8 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D256C020EC4A6696D3E18665 /* Readme.swift */; }; FCE80FC6A44E1A3360CE0ABD /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0C55708836E9855934DCA7B /* Readme.swift */; }; /* End PBXBuildFile section */ @@ -78,6 +85,7 @@ /* Begin PBXFileReference section */ 0041AFC6938603AA8E408C85 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 0078638D97BB480D82B18D6C /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; + 03168F8FCB98FF73CC62A64A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 0340E1FFDDBC0C9D72FCD4D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1874579DE7E406E73A3CB3A1 /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; 2D50867A0DB9C8686F63A371 /* InAppMessages.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = InAppMessages.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -95,11 +103,14 @@ A0C55708836E9855934DCA7B /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; A37E3B0FA418AB843D26F7F9 /* PushNotifications.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = PushNotifications.app; sourceTree = BUILT_PRODUCTS_DIR; }; A77BCD95413ADCF8578235CE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + AC13846BDD825F17C9C23B9B /* SDWebImageGIFViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageGIFViewProvider.swift; sourceTree = ""; }; AF74213486BAEC4E0C94F02D /* SDWebImageGIFViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageGIFViewProvider.swift; sourceTree = ""; }; BA064323F91A22F4E14CAE9D /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; BE9A4EACB9451C069A63F43F /* AuthenticationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationManager.swift; sourceTree = ""; }; C938B015CECD020B6FA2206F /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; + C9C1B6158983BCB0C593DAE4 /* ContentCards.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = ContentCards.app; sourceTree = BUILT_PRODUCTS_DIR; }; D256C020EC4A6696D3E18665 /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; + ED6428DF0330FE85A4FF7135 /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; FBB4D400F7D3A993E0BD0841 /* Location.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Location.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -151,6 +162,7 @@ children = ( EB043806B7BB06DCA25E0177 /* Common */, 51FBDA444B2130ED53F050CB /* Analytics */, + EE1BF73C17001D2912250407 /* ContentCards */, 9AA2DA5B878C1242DBDD0410 /* InAppMessages */, 4F38F2ACA591DCA04EF16BB1 /* Location */, E956091874C7F622B207B35A /* PushNotifications */, @@ -174,6 +186,7 @@ isa = PBXGroup; children = ( 2DF8C1CB2B5756E23B0BE275 /* Analytics.app */, + C9C1B6158983BCB0C593DAE4 /* ContentCards.app */, 2D50867A0DB9C8686F63A371 /* InAppMessages.app */, FBB4D400F7D3A993E0BD0841 /* Location.app */, A37E3B0FA418AB843D26F7F9 /* PushNotifications.app */, @@ -242,6 +255,17 @@ path = Sources/Common; sourceTree = ""; }; + EE1BF73C17001D2912250407 /* ContentCards */ = { + isa = PBXGroup; + children = ( + 03168F8FCB98FF73CC62A64A /* AppDelegate.swift */, + ED6428DF0330FE85A4FF7135 /* Readme.swift */, + AC13846BDD825F17C9C23B9B /* SDWebImageGIFViewProvider.swift */, + ); + name = ContentCards; + path = Sources/ContentCards; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -280,6 +304,22 @@ productReference = A37E3B0FA418AB843D26F7F9 /* PushNotifications.app */; productType = "com.apple.product-type.application"; }; + 9097C3EC847DC7AE4B1CA969 /* ContentCards */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9D35AC44AE90C1C27CBF8577 /* Build configuration list for PBXNativeTarget "ContentCards" */; + buildPhases = ( + C7726F7D79339C85CB8B1FDB /* Sources */, + 0607C6FAE74CA8A0F8E5224B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ContentCards; + productName = ContentCards; + productReference = C9C1B6158983BCB0C593DAE4 /* ContentCards.app */; + productType = "com.apple.product-type.application"; + }; C7435EF6816986F173CAB4E9 /* PushNotificationsContentExtension */ = { isa = PBXNativeTarget; buildConfigurationList = 03D1E503F9863A03CB17E53A /* Build configuration list for PBXNativeTarget "PushNotificationsContentExtension" */; @@ -367,6 +407,7 @@ projectRoot = ""; targets = ( DB51FDF40865152F0A25DE3B /* Analytics */, + 9097C3EC847DC7AE4B1CA969 /* ContentCards */, 1A7E9979FC152DE4F7D5DC2A /* InAppMessages */, F260478D9F71B63E53D49D3E /* Location */, 726E40C6A0B9D7595B53FE5E /* PushNotifications */, @@ -377,6 +418,15 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 0607C6FAE74CA8A0F8E5224B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F49C339077170329AC8926A3 /* Assets.xcassets in Resources */, + 30D7A569B1A652D533C6DFF4 /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 324A65B0C1348D482EBD2C51 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -445,6 +495,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C7726F7D79339C85CB8B1FDB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7489ACB9F90DED02AA3B64EE /* AppDelegate.swift in Sources */, + 243D978233307D328A4A1EE4 /* Credentials.swift in Sources */, + C38B9EA3B93DEF499E7C6FDE /* Readme.swift in Sources */, + F7BCA95A9B52DF8721CEC8EF /* ReadmeViewController.swift in Sources */, + 5B99366856EF616F55DB7F14 /* SDWebImageGIFViewProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D0E6AF1EADD2B87E3CE9991E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -521,6 +583,23 @@ }; name = Release; }; + 0FF3E9103BDC67C9723BD0CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = Sources/ContentCards/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.braze.ContentCards; + PRODUCT_NAME = ContentCards; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; 13690EEFD7512C8A754DA9A1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -589,6 +668,23 @@ }; name = Release; }; + 508970482D723EDFFD096AB1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = Sources/ContentCards/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.braze.ContentCards; + PRODUCT_NAME = ContentCards; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 7E69BEDFFB0C6607D3722A66 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -866,6 +962,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + 9D35AC44AE90C1C27CBF8577 /* Build configuration list for PBXNativeTarget "ContentCards" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0FF3E9103BDC67C9723BD0CD /* Debug */, + 508970482D723EDFFD096AB1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; C8DA1DB107E7EE64BBBAEBE7 /* Build configuration list for PBXNativeTarget "InAppMessages" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Examples/Examples-CocoaPods.xcworkspace/contents.xcworkspacedata b/Examples/Examples-CocoaPods.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..1757026303 --- /dev/null +++ b/Examples/Examples-CocoaPods.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Examples/Examples-SwiftPM.xcodeproj/project.pbxproj b/Examples/Examples-SwiftPM.xcodeproj/project.pbxproj index 5918755799..f1017cd0d2 100644 --- a/Examples/Examples-SwiftPM.xcodeproj/project.pbxproj +++ b/Examples/Examples-SwiftPM.xcodeproj/project.pbxproj @@ -7,13 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 02C558571508A77542FBC26C /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 0D08CDAB0AE57DFC38F9B590 /* SDWebImage */; }; 0A0451BD984D289E9A9F11F0 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A7EC3A55D550F5ADC29F4F4 /* Readme.swift */; }; 13EC4FC32BAC3979624BCDA2 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C3766DAA0E86C999794A8F /* ReadmeViewController.swift */; }; 176536B39FED72ECC2028BFC /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 0BF370808D2F5C21870331A0 /* SDWebImage */; }; + 241F4F1F41D820C55156841D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0FCD17EEF41713F18F4CA702 /* Assets.xcassets */; }; 24400D6B97710B109BDD8A60 /* BrazePushStory in Frameworks */ = {isa = PBXBuildFile; productRef = 5133F5A2D91882A4AE649D4D /* BrazePushStory */; }; 2C47D73C2D8F2F68DF83612B /* BrazeNotificationService in Frameworks */ = {isa = PBXBuildFile; productRef = 47F3E12B1114B00B4CB70C60 /* BrazeNotificationService */; }; 2D0E2F5133A8E77855FE62AF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE5A233316273D84DDDECFB /* AppDelegate.swift */; }; 312590E9832D85052B8D9759 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = F4168F1485105BAAD54B813C /* BrazeKit */; }; + 33C0F7AEF567B240303C75A8 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C3766DAA0E86C999794A8F /* ReadmeViewController.swift */; }; 4484E207BCFD002822F2BB7D /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE00D10FF658213E5A00188D /* NotificationViewController.swift */; }; 4FCC18D09440113FCC444C49 /* PushNotificationsContentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A7B092ACAEC915379923328F /* PushNotificationsContentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 50D8D4EB19BF8DB20DF34B8D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0FCD17EEF41713F18F4CA702 /* Assets.xcassets */; }; @@ -21,8 +24,10 @@ 53101EC7B6D20D35089EFCD0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D680CBC8E96C58FD5F8BA5D /* AppDelegate.swift */; }; 5C0D56998D46869E0617E5A4 /* BrazeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 9AF0AF048F752A5C62317470 /* BrazeUI */; }; 66308A8D024226F21FA21486 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C3766DAA0E86C999794A8F /* ReadmeViewController.swift */; }; + 677C7AE5FD7BEF2DD1B0016B /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCAE9DDCCBE7557171C3574D /* Credentials.swift */; }; 69EFC44917551E618D408078 /* AuthenticationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591F3570467FB230FB58226B /* AuthenticationManager.swift */; }; 6DA15916A0F0F7ECF3968E42 /* PushNotificationsServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = B5666DA142189EEEDFAFB41F /* PushNotificationsServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6FAFF3BA371E41638E044127 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 89598718421C181B57346379 /* BrazeKit */; }; 7029F05B818AFEB6F82A9443 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 415A55D2760CB84A0CC3A13B /* LaunchScreen.storyboard */; }; 7433145D038D07087F62D331 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7451EF2F037063F70104E13A /* Readme.swift */; }; 76D388C71E54AC6F93A16056 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B5CF8D1D25040DF4009F8D /* Readme.swift */; }; @@ -32,8 +37,10 @@ 8FEC271CD3BBE8918EAC74D0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0FCD17EEF41713F18F4CA702 /* Assets.xcassets */; }; 904106D5D0DCB422BFFC0702 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9BAE4406D94C39537917176F /* BrazeKit */; }; 90DE4CA52AAA711D9995399F /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCAE9DDCCBE7557171C3574D /* Credentials.swift */; }; + 9CE6B2D9DC0F3B277094AEB9 /* BrazeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 26C11D75220DCD8456AEAA39 /* BrazeUI */; }; AA95C8A6110DE76895C734A7 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCAE9DDCCBE7557171C3574D /* Credentials.swift */; }; ABBB4C198868F5EDC7A3EB0D /* SDWebImageGIFViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC100D3BC83C040C950594D /* SDWebImageGIFViewProvider.swift */; }; + AD0FB326BFA4C3E7A64469FA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 415A55D2760CB84A0CC3A13B /* LaunchScreen.storyboard */; }; AE9B5583963142DD6C7E2B7F /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F498CAD1B487B718A6064 /* NotificationService.swift */; }; AF2CC1B40270C6B61BCB36F3 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B0D325AE1E7E717D151B9B6B /* UserNotificationsUI.framework */; }; B010CCF390009B9650541D34 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C3766DAA0E86C999794A8F /* ReadmeViewController.swift */; }; @@ -44,12 +51,15 @@ DBFF6E62C94BFDCEDEC7FAED /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C3766DAA0E86C999794A8F /* ReadmeViewController.swift */; }; DD04470BAE6FBDC467DBDA53 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 40864C1ED6466B40F23B72D8 /* BrazeKit */; }; DDFC2FEE20F0CF175328CED0 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FF4447693460C3B6034C1C /* Readme.swift */; }; + DF74A0BB77F6BE2B9B05FF0A /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38C711914A00B6ED0285D7E /* Readme.swift */; }; E25FA2C2B7AD9436C1E31634 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 415A55D2760CB84A0CC3A13B /* LaunchScreen.storyboard */; }; E784AA64821A0E83F5F90128 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCAE9DDCCBE7557171C3574D /* Credentials.swift */; }; E7A502FBBA81B6005B62EE82 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCAE9DDCCBE7557171C3574D /* Credentials.swift */; }; + E7F0FB91F4C78861E58DAA8C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0A55E8282ADE8756715B6A /* AppDelegate.swift */; }; EB1B1B3FB65B2B31821EC224 /* CheckoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631DCED7E638A6BD4C9D95C2 /* CheckoutViewController.swift */; }; EC57F25FE46BBD05EA33C939 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08CE7C19F258AF40B394B358 /* UserNotifications.framework */; }; EF5EF251D34064B14F500469 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 09701E67CAE64FCE46458059 /* BrazeKit */; }; + FAAC7851B9F24C8DBBF9F138 /* SDWebImageGIFViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A33772C116D28F53CFB986 /* SDWebImageGIFViewProvider.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -92,6 +102,7 @@ 1D680CBC8E96C58FD5F8BA5D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 220432BEC0EA7628608EA5BA /* braze-swift-sdk */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "braze-swift-sdk"; path = ..; sourceTree = SOURCE_ROOT; }; 4AC100D3BC83C040C950594D /* SDWebImageGIFViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageGIFViewProvider.swift; sourceTree = ""; }; + 4B0A55E8282ADE8756715B6A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 591F3570467FB230FB58226B /* AuthenticationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationManager.swift; sourceTree = ""; }; 61323FF27FE35756D97CED2B /* Location.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Location.app; sourceTree = BUILT_PRODUCTS_DIR; }; 631DCED7E638A6BD4C9D95C2 /* CheckoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutViewController.swift; sourceTree = ""; }; @@ -99,12 +110,15 @@ 6A7EC3A55D550F5ADC29F4F4 /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; 7451EF2F037063F70104E13A /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; 8D441B5B5424C3E129B2DA54 /* PushNotifications.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = PushNotifications.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 99A33772C116D28F53CFB986 /* SDWebImageGIFViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageGIFViewProvider.swift; sourceTree = ""; }; 9AE5A233316273D84DDDECFB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A7B092ACAEC915379923328F /* PushNotificationsContentExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = PushNotificationsContentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; A7B5CF8D1D25040DF4009F8D /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; B0D325AE1E7E717D151B9B6B /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; B1FF4447693460C3B6034C1C /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; B5666DA142189EEEDFAFB41F /* PushNotificationsServiceExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = PushNotificationsServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + B7F06535E19BAEA9F5B73F86 /* ContentCards.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = ContentCards.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C38C711914A00B6ED0285D7E /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; CCAE9DDCCBE7557171C3574D /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; CE00D10FF658213E5A00188D /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; D63F498CAD1B487B718A6064 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; @@ -159,6 +173,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E70CCCD8CEE5D68B1E92800 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6FAFF3BA371E41638E044127 /* BrazeKit in Frameworks */, + 9CE6B2D9DC0F3B277094AEB9 /* BrazeUI in Frameworks */, + 02C558571508A77542FBC26C /* SDWebImage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FA7EB9315B4886B030BB5B6A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -171,6 +195,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 234FC0FDE1EB1D2B1EE92CDA /* ContentCards */ = { + isa = PBXGroup; + children = ( + 4B0A55E8282ADE8756715B6A /* AppDelegate.swift */, + C38C711914A00B6ED0285D7E /* Readme.swift */, + 99A33772C116D28F53CFB986 /* SDWebImageGIFViewProvider.swift */, + ); + name = ContentCards; + path = Sources/ContentCards; + sourceTree = ""; + }; 256C64FD5A9C547C95ED205A /* Location */ = { isa = PBXGroup; children = ( @@ -256,6 +291,7 @@ isa = PBXGroup; children = ( ED657ED06A52B67E6F53309B /* Analytics.app */, + B7F06535E19BAEA9F5B73F86 /* ContentCards.app */, 65785E585247CC7A8661ED6B /* InAppMessages.app */, 61323FF27FE35756D97CED2B /* Location.app */, 8D441B5B5424C3E129B2DA54 /* PushNotifications.app */, @@ -271,6 +307,7 @@ E3AF9F206199B993ACB3E63D /* Packages */, B4F25C051708E808116165D4 /* Common */, 258F28BC46EE3F2D3E079C37 /* Analytics */, + 234FC0FDE1EB1D2B1EE92CDA /* ContentCards */, A3A2735A5DC208B24378D437 /* InAppMessages */, 256C64FD5A9C547C95ED205A /* Location */, 67175574AAFF46A4632B42DA /* PushNotifications */, @@ -385,6 +422,28 @@ productReference = 8D441B5B5424C3E129B2DA54 /* PushNotifications.app */; productType = "com.apple.product-type.application"; }; + DEE99E05C6F822DC78AEE272 /* ContentCards */ = { + isa = PBXNativeTarget; + buildConfigurationList = DAC0A7E698E628D993F35934 /* Build configuration list for PBXNativeTarget "ContentCards" */; + buildPhases = ( + 6089EE24E3EFFEC175CB9A6A /* Sources */, + 1F3BD016FE47788CA94B7A4A /* Resources */, + 6E70CCCD8CEE5D68B1E92800 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ContentCards; + packageProductDependencies = ( + 89598718421C181B57346379 /* BrazeKit */, + 26C11D75220DCD8456AEAA39 /* BrazeUI */, + 0D08CDAB0AE57DFC38F9B590 /* SDWebImage */, + ); + productName = ContentCards; + productReference = B7F06535E19BAEA9F5B73F86 /* ContentCards.app */; + productType = "com.apple.product-type.application"; + }; E59DB4201FEAE1A212979375 /* PushNotificationsContentExtension */ = { isa = PBXNativeTarget; buildConfigurationList = 538A7D3F62B432A07C91FD40 /* Build configuration list for PBXNativeTarget "PushNotificationsContentExtension" */; @@ -451,6 +510,7 @@ projectRoot = ""; targets = ( 0642C7DCDAD9AB0330096707 /* Analytics */, + DEE99E05C6F822DC78AEE272 /* ContentCards */, 5BE2DFC918DF0CC5E69FE387 /* InAppMessages */, FA78BE27C24F0E569EC66970 /* Location */, B64E1C1C2CDBA52874450596 /* PushNotifications */, @@ -470,6 +530,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 1F3BD016FE47788CA94B7A4A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 241F4F1F41D820C55156841D /* Assets.xcassets in Resources */, + AD0FB326BFA4C3E7A64469FA /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3D8D07DBB7AF1C865DA26D8E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -519,6 +588,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6089EE24E3EFFEC175CB9A6A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E7F0FB91F4C78861E58DAA8C /* AppDelegate.swift in Sources */, + 677C7AE5FD7BEF2DD1B0016B /* Credentials.swift in Sources */, + DF74A0BB77F6BE2B9B05FF0A /* Readme.swift in Sources */, + 33C0F7AEF567B240303C75A8 /* ReadmeViewController.swift in Sources */, + FAAC7851B9F24C8DBBF9F138 /* SDWebImageGIFViewProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 629101DE070207860B0FF0B5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -642,6 +723,23 @@ }; name = Release; }; + 338E775FC51FCA458B2B4AFB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = Sources/ContentCards/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.braze.ContentCards; + PRODUCT_NAME = ContentCards; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; 35E8D78DF517B11959723F4D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -706,6 +804,23 @@ }; name = Debug; }; + 4C1CEEB16B22B01668229861 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = Sources/ContentCards/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.braze.ContentCards; + PRODUCT_NAME = ContentCards; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 56FADC974A57962ED605AB39 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -959,6 +1074,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + DAC0A7E698E628D993F35934 /* Build configuration list for PBXNativeTarget "ContentCards" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 338E775FC51FCA458B2B4AFB /* Debug */, + 4C1CEEB16B22B01668229861 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; F76B079D8771880EE7A2C01D /* Build configuration list for PBXNativeTarget "InAppMessages" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1000,6 +1124,15 @@ package = 9928D5150C45879A982BA1C6 /* XCRemoteSwiftPackageReference "SDWebImage" */; productName = SDWebImage; }; + 0D08CDAB0AE57DFC38F9B590 /* SDWebImage */ = { + isa = XCSwiftPackageProductDependency; + package = 9928D5150C45879A982BA1C6 /* XCRemoteSwiftPackageReference "SDWebImage" */; + productName = SDWebImage; + }; + 26C11D75220DCD8456AEAA39 /* BrazeUI */ = { + isa = XCSwiftPackageProductDependency; + productName = BrazeUI; + }; 40864C1ED6466B40F23B72D8 /* BrazeKit */ = { isa = XCSwiftPackageProductDependency; productName = BrazeKit; @@ -1012,6 +1145,10 @@ isa = XCSwiftPackageProductDependency; productName = BrazePushStory; }; + 89598718421C181B57346379 /* BrazeKit */ = { + isa = XCSwiftPackageProductDependency; + productName = BrazeKit; + }; 9AF0AF048F752A5C62317470 /* BrazeUI */ = { isa = XCSwiftPackageProductDependency; productName = BrazeUI; diff --git a/Examples/Examples-SwiftPM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples-SwiftPM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 33f603edd9..6e55ff0673 100644 --- a/Examples/Examples-SwiftPM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples-SwiftPM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,14 +1,70 @@ { - "pins" : [ - { - "identity" : "sdwebimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SDWebImage/SDWebImage", - "state" : { - "revision" : "3e48cb68d8e668d146dc59c73fb98cb628616236", - "version" : "5.13.2" + "object": { + "pins": [ + { + "package": "SDWebImage", + "repositoryURL": "https://github.com/SDWebImage/SDWebImage", + "state": { + "branch": null, + "revision": "3e48cb68d8e668d146dc59c73fb98cb628616236", + "version": "5.13.2" + } + }, + { + "package": "BrazeArchitecture", + "repositoryURL": "git@github.com:braze-inc/swift-architecture.git", + "state": { + "branch": "29aa1d2", + "revision": "29aa1d2db79affcc2d43b25a3ecfb9f63e1c8044", + "version": null + } + }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser", + "state": { + "branch": null, + "revision": "df9ee6676cd5b3bf5b330ec7568a5644f547201b", + "version": "1.1.3" + } + }, + { + "package": "SnapshotTesting", + "repositoryURL": "https://github.com/braze-inc/swift-snapshot-testing.git", + "state": { + "branch": "braze-changes", + "revision": "2b099d50fa337cf88d7b3d9c65880d0f905976fb", + "version": null + } + }, + { + "package": "swift-tools-support-core", + "repositoryURL": "https://github.com/apple/swift-tools-support-core", + "state": { + "branch": "swift-5.5.2-RELEASE", + "revision": "3b586ce12865db205081acdcea79fe5509b28152", + "version": null + } + }, + { + "package": "SwiftSoup", + "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", + "state": { + "branch": null, + "revision": "6778575285177365cbad3e5b8a72f2a20583cfec", + "version": "2.4.3" + } + }, + { + "package": "VideoKit", + "repositoryURL": "https://github.com/lowip/VideoKit", + "state": { + "branch": null, + "revision": "615653151c8fa7f7c191cd49939e0f69daf5aff8", + "version": "0.1.4" + } } - } - ], - "version" : 2 + ] + }, + "version": 1 } diff --git a/Examples/Podfile b/Examples/Podfile index e35f7574b3..848d09426d 100644 --- a/Examples/Podfile +++ b/Examples/Podfile @@ -11,8 +11,17 @@ target 'InAppMessages' do platform :ios, '10.0' pod 'BrazeKit' pod 'BrazeUI' - # SDWebImage is optional. BrazeUI requires a third party library to display gif images. - # See https://braze-inc.github.io/braze-swift-sdk/documentation/braze/gif-support + # SDWebImage is optional. BrazeUI requires a third party library to display GIF images. + # See https://braze-inc.github.io/braze-swift-sdk/documentation/braze/gif-support-integrations + pod 'SDWebImage', :modular_headers => true +end + +target 'ContentCards' do + platform :ios, '10.0' + pod 'BrazeKit' + pod 'BrazeUI' + # SDWebImage is optional. BrazeUI requires a third party library to display GIF images. + # See https://braze-inc.github.io/braze-swift-sdk/documentation/braze/gif-support-integrations pod 'SDWebImage', :modular_headers => true end diff --git a/Examples/Sources/Analytics/AppDelegate.swift b/Examples/Sources/Analytics/AppDelegate.swift index dacc66e459..7558036354 100644 --- a/Examples/Sources/Analytics/AppDelegate.swift +++ b/Examples/Sources/Analytics/AppDelegate.swift @@ -1,5 +1,5 @@ -import UIKit import BrazeKit +import UIKit // MARK: - Configure Braze diff --git a/Examples/Sources/Analytics/AuthenticationManager.swift b/Examples/Sources/Analytics/AuthenticationManager.swift index 2b5c0aa55c..72ab46eb06 100644 --- a/Examples/Sources/Analytics/AuthenticationManager.swift +++ b/Examples/Sources/Analytics/AuthenticationManager.swift @@ -1,5 +1,5 @@ -import Foundation import BrazeKit +import Foundation class AuthenticationManager { diff --git a/Examples/Sources/Analytics/Readme.swift b/Examples/Sources/Analytics/Readme.swift index 92042c5dc3..b7b57ce8b3 100644 --- a/Examples/Sources/Analytics/Readme.swift +++ b/Examples/Sources/Analytics/Readme.swift @@ -53,7 +53,7 @@ func createCheckoutViewController() -> (UINavigationController, CheckoutViewCont let productsIds = [ UUID().uuidString, UUID().uuidString, - UUID().uuidString + UUID().uuidString, ] let checkoutViewController = CheckoutViewController() diff --git a/Examples/Sources/Common/ReadmeViewController.swift b/Examples/Sources/Common/ReadmeViewController.swift index 57f3046d8f..0ab4fffdaa 100644 --- a/Examples/Sources/Common/ReadmeViewController.swift +++ b/Examples/Sources/Common/ReadmeViewController.swift @@ -16,7 +16,8 @@ final class ReadmeViewController: UITableViewController { textView.font = .monospacedSystemFont(ofSize: 12, weight: .regular) } #elseif os(tvOS) - textView.textContainerInset = UIEdgeInsets(top: 0, left: 16 * 6, bottom: 16 * 4, right: 16 * 6) + textView.textContainerInset = UIEdgeInsets( + top: 0, left: 16 * 6, bottom: 16 * 4, right: 16 * 6) if #available(tvOS 13.0, *) { textView.font = .monospacedSystemFont(ofSize: 30, weight: .regular) } @@ -29,6 +30,9 @@ final class ReadmeViewController: UITableViewController { self.actions = actions super.init(style: .grouped) + // Set title + title = Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String + // Set readme text readmeTextView.text = """ @@ -52,7 +56,8 @@ final class ReadmeViewController: UITableViewController { override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() - let size = readmeTextView.systemLayoutSizeFitting(.init(width: tableView.bounds.width, height: 0)) + let size = readmeTextView.systemLayoutSizeFitting( + .init(width: tableView.bounds.width, height: 0)) if readmeTextView.frame.height != size.height { readmeTextView.frame.size.height = size.height } @@ -64,7 +69,8 @@ final class ReadmeViewController: UITableViewController { actions.count } - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? + { "Actions" } @@ -73,7 +79,8 @@ final class ReadmeViewController: UITableViewController { cellForRowAt indexPath: IndexPath ) -> UITableViewCell { let identifier = "cellIdentifier" - let cell = tableView.dequeueReusableCell(withIdentifier: identifier) + let cell = + tableView.dequeueReusableCell(withIdentifier: identifier) ?? UITableViewCell(style: .subtitle, reuseIdentifier: identifier) cell.textLabel?.text = actions[indexPath.row].0 cell.detailTextLabel?.text = actions[indexPath.row].1 @@ -92,8 +99,11 @@ final class ReadmeViewController: UITableViewController { // MARK: - AutoReadme private var _window: UIWindow? = { + let readmeViewController = ReadmeViewController(readme: readme, actions: actions) + let navigationController = UINavigationController(rootViewController: readmeViewController) + let window = UIWindow(frame: UIScreen.main.bounds) - window.rootViewController = ReadmeViewController(readme: readme, actions: actions) + window.rootViewController = navigationController return window }() diff --git a/Examples/Sources/ContentCards/AppDelegate.swift b/Examples/Sources/ContentCards/AppDelegate.swift new file mode 100644 index 0000000000..d087a1295d --- /dev/null +++ b/Examples/Sources/ContentCards/AppDelegate.swift @@ -0,0 +1,94 @@ +import BrazeKit +import BrazeUI +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + static var braze: Braze? = nil + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Setup Braze + var configuration = Braze.Configuration(apiKey: brazeApiKey, endpoint: brazeEndpoint) + configuration.logger.level = .info + let braze = Braze(configuration: configuration) + AppDelegate.braze = braze + + // - GIF support + BrazeUI.gifViewProvider = .sdWebImage + + window?.makeKeyAndVisible() + return true + } + + // MARK: - Displaying Content Cards + + func pushContentCardsViewController() { + guard let braze = Self.braze, + let navigationController = window?.rootViewController as? UINavigationController + else { return } + + // Create a content card view controller that can be pushed onto a navigation stack. + let viewController = BrazeContentCardUI.ViewController(braze: braze) + // Set the delegate + viewController.delegate = self + // Push it onto the navigation stack + navigationController.pushViewController(viewController, animated: true) + } + + func presentModalContentCardsViewController() { + guard let braze = Self.braze, + let navigationController = window?.rootViewController as? UINavigationController + else { return } + + // Create a content card modal view controller that can be presented modally. + let modalViewController = BrazeContentCardUI.ModalViewController(braze: braze) + // Set the delegate + modalViewController.viewController.delegate = self + // Present modally + navigationController.present(modalViewController, animated: true) + } + + func presentModalContentCardsViewControllerCustomized() { + guard let braze = Self.braze, + let navigationController = window?.rootViewController as? UINavigationController + else { return } + + // Create custom attributes to change how the content cards view controller behaves and appears. + var attributes = BrazeContentCardUI.ViewController.Attributes.defaults + attributes.pullToRefresh = false + attributes.cellAttributes.cornerRadius = 16 + attributes.cellAttributes.highlightColor = .systemGreen + attributes.cellAttributes.titleFont = .italicSystemFont(ofSize: 16) + attributes.cellAttributes.shadow = nil + + // Create a content card modal view controller using the custom attributes. + let modalViewController = BrazeContentCardUI.ModalViewController( + braze: braze, + attributes: attributes + ) + // Set the delegate + modalViewController.viewController.delegate = self + // Present modally + navigationController.present(modalViewController, animated: true) + } + +} + +// MARK: - Content Cards delegate + +extension AppDelegate: BrazeContentCardUIViewControllerDelegate { + + func contentCard( + _ controller: BrazeContentCardUI.ViewController, + shouldProcess clickAction: Braze.ContentCard.ClickAction, + card: Braze.ContentCard + ) -> Bool { + // Intercept the content card click action here + return true + } + +} diff --git a/Examples/Sources/ContentCards/Readme.swift b/Examples/Sources/ContentCards/Readme.swift new file mode 100644 index 0000000000..8df25ef91a --- /dev/null +++ b/Examples/Sources/ContentCards/Readme.swift @@ -0,0 +1,46 @@ +import BrazeKit +import UIKit + +let readme = + """ + This sample presents how to use the Braze provided content cards UI: + + - AppDelegate.swift: + - Content cards UI presentation + - Content cards UI delegate + - SDWebImageGIFViewProvider.swift: + - Use SDWebImage to provide GIF support to the Braze UI components + """ + +let actions: [(String, String, (ReadmeViewController) -> Void)] = [ + ( + "Push content cards view controller", + "", + pushContentCardsViewController + ), + ( + "Present modal content cards view controller", + "", + presentModalContentCardsViewController + ), + ( + "Present modal content cards view controller", + "(customized)", + presentModalContentCardsViewControllerCustomized + ), +] + +// MARK: - Internal + +func pushContentCardsViewController(_ viewController: ReadmeViewController) { + (UIApplication.shared.delegate as? AppDelegate)?.pushContentCardsViewController() +} + +func presentModalContentCardsViewController(_ viewController: ReadmeViewController) { + (UIApplication.shared.delegate as? AppDelegate)?.presentModalContentCardsViewController() +} + +func presentModalContentCardsViewControllerCustomized(_ viewController: ReadmeViewController) { + (UIApplication.shared.delegate as? AppDelegate)? + .presentModalContentCardsViewControllerCustomized() +} diff --git a/Examples/Sources/ContentCards/SDWebImageGIFViewProvider.swift b/Examples/Sources/ContentCards/SDWebImageGIFViewProvider.swift new file mode 100644 index 0000000000..5858c0ea5a --- /dev/null +++ b/Examples/Sources/ContentCards/SDWebImageGIFViewProvider.swift @@ -0,0 +1,20 @@ +import BrazeUI +import SDWebImage + +extension GIFViewProvider { + + /// A GIF view provider using [SDWebImage](https://github.com/SDWebImage/SDWebImage) as a + /// rendering library. + static let sdWebImage = Self( + view: { SDAnimatedImageView(image: image(for: $0)) }, + updateView: { ($0 as? SDAnimatedImageView)?.image = image(for: $1) } + ) + + private static func image(for url: URL?) -> UIImage? { + guard let url = url else { return nil } + return url.pathExtension == "gif" + ? SDAnimatedImage(contentsOfFile: url.path) + : UIImage(contentsOfFile: url.path) + } + +} diff --git a/Examples/Sources/InAppMessages/AppDelegate.swift b/Examples/Sources/InAppMessages/AppDelegate.swift index 52fc274f18..d9a9484866 100644 --- a/Examples/Sources/InAppMessages/AppDelegate.swift +++ b/Examples/Sources/InAppMessages/AppDelegate.swift @@ -1,6 +1,6 @@ -import UIKit import BrazeKit import BrazeUI +import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -17,8 +17,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let braze = Braze(configuration: configuration) AppDelegate.braze = braze - // - InAppMessage UI + // - GIF support BrazeUI.gifViewProvider = .sdWebImage + + // - InAppMessage UI let inAppMessageUI = BrazeInAppMessageUI() inAppMessageUI.delegate = self braze.inAppMessagePresenter = inAppMessageUI @@ -58,4 +60,3 @@ extension AppDelegate: BrazeInAppMessageUIDelegate { } } - diff --git a/Examples/Sources/InAppMessages/Readme.swift b/Examples/Sources/InAppMessages/Readme.swift index 0814cc2334..bc19f4fb7b 100644 --- a/Examples/Sources/InAppMessages/Readme.swift +++ b/Examples/Sources/InAppMessages/Readme.swift @@ -1,5 +1,5 @@ -import UIKit import BrazeKit +import UIKit let readme = """ @@ -9,7 +9,7 @@ let readme = - In-app message UI configuration - In-app message UI delegate - SDWebImageGIFViewProvider.swift: - - Use SDWebImage to provide gif support to the in-app message UI + - Use SDWebImage to provide GIF support to the Braze UI components """ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ @@ -22,7 +22,7 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ "Present local modal in-app message", "", localModal - ) + ), ] // MARK: - Internal @@ -47,4 +47,3 @@ func localModal(_ viewController: ReadmeViewController) { ) AppDelegate.braze?.inAppMessagePresenter?.present(message: modal) } - diff --git a/Examples/Sources/InAppMessages/SDWebImageGIFViewProvider.swift b/Examples/Sources/InAppMessages/SDWebImageGIFViewProvider.swift index bfa9f25245..5858c0ea5a 100644 --- a/Examples/Sources/InAppMessages/SDWebImageGIFViewProvider.swift +++ b/Examples/Sources/InAppMessages/SDWebImageGIFViewProvider.swift @@ -3,7 +3,7 @@ import SDWebImage extension GIFViewProvider { - /// A gif view provider using [SDWebImage](https://github.com/SDWebImage/SDWebImage) as a + /// A GIF view provider using [SDWebImage](https://github.com/SDWebImage/SDWebImage) as a /// rendering library. static let sdWebImage = Self( view: { SDAnimatedImageView(image: image(for: $0)) }, @@ -13,8 +13,8 @@ extension GIFViewProvider { private static func image(for url: URL?) -> UIImage? { guard let url = url else { return nil } return url.pathExtension == "gif" - ? SDAnimatedImage(contentsOfFile: url.path) - : UIImage(contentsOfFile: url.path) + ? SDAnimatedImage(contentsOfFile: url.path) + : UIImage(contentsOfFile: url.path) } } diff --git a/Examples/Sources/Location/AppDelegate.swift b/Examples/Sources/Location/AppDelegate.swift index 1d5ae9e914..9f8cb6e23c 100644 --- a/Examples/Sources/Location/AppDelegate.swift +++ b/Examples/Sources/Location/AppDelegate.swift @@ -1,6 +1,6 @@ -import UIKit import BrazeKit import BrazeLocation +import UIKit // MARK: - Configure Braze diff --git a/Examples/Sources/Location/Readme.swift b/Examples/Sources/Location/Readme.swift index 04d07a7569..c61f7f44e0 100644 --- a/Examples/Sources/Location/Readme.swift +++ b/Examples/Sources/Location/Readme.swift @@ -9,14 +9,14 @@ let readme = """ #if os(iOS) -let actions: [(String, String, (ReadmeViewController) -> Void)] = [ - (#"Request "always" authorization"#, "", requestAlwaysAuthorization), - (#"Request "when in use" authorization"#, "", requestWhenInUseAuthorization), -] + let actions: [(String, String, (ReadmeViewController) -> Void)] = [ + (#"Request "always" authorization"#, "", requestAlwaysAuthorization), + (#"Request "when in use" authorization"#, "", requestWhenInUseAuthorization), + ] #elseif os(tvOS) -let actions: [(String, String, (ReadmeViewController) -> Void)] = [ - (#"Request "when in use" authorization"#, "", requestWhenInUseAuthorization), -] + let actions: [(String, String, (ReadmeViewController) -> Void)] = [ + (#"Request "when in use" authorization"#, "", requestWhenInUseAuthorization) + ] #endif // MARK: - Internal @@ -24,9 +24,9 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ let locationManager = CLLocationManager() #if os(iOS) -func requestAlwaysAuthorization(_ viewController: ReadmeViewController) { - locationManager.requestAlwaysAuthorization() -} + func requestAlwaysAuthorization(_ viewController: ReadmeViewController) { + locationManager.requestAlwaysAuthorization() + } #endif func requestWhenInUseAuthorization(_ viewController: ReadmeViewController) { diff --git a/Examples/Sources/ObjC/Info.plist b/Examples/Sources/ObjC/Info.plist new file mode 100644 index 0000000000..286166f747 --- /dev/null +++ b/Examples/Sources/ObjC/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + ObjC + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + UILaunchStoryboardName + LaunchScreen + + diff --git a/Examples/Sources/PushNotifications/AppDelegate.swift b/Examples/Sources/PushNotifications/AppDelegate.swift index 4b9b4793c9..5db5f5a267 100644 --- a/Examples/Sources/PushNotifications/AppDelegate.swift +++ b/Examples/Sources/PushNotifications/AppDelegate.swift @@ -1,6 +1,6 @@ +import BrazeKit import UIKit import UserNotifications -import BrazeKit @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -47,13 +47,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable : Any], + didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { - if let braze = AppDelegate.braze, braze.notifications.handleBackgroundNotification( - userInfo: userInfo, - fetchCompletionHandler: completionHandler - ) { + if let braze = AppDelegate.braze, + braze.notifications.handleBackgroundNotification( + userInfo: userInfo, + fetchCompletionHandler: completionHandler + ) + { return } completionHandler(.noData) @@ -70,10 +72,12 @@ extension AppDelegate: UNUserNotificationCenterDelegate { didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { - if let braze = AppDelegate.braze, braze.notifications.handleUserNotification( - response: response, - withCompletionHandler: completionHandler - ) { + if let braze = AppDelegate.braze, + braze.notifications.handleUserNotification( + response: response, + withCompletionHandler: completionHandler + ) + { return } completionHandler() diff --git a/Examples/Sources/PushNotifications/Readme.swift b/Examples/Sources/PushNotifications/Readme.swift index 4383602668..dbcb148208 100644 --- a/Examples/Sources/PushNotifications/Readme.swift +++ b/Examples/Sources/PushNotifications/Readme.swift @@ -1,4 +1,3 @@ - let readme = """ This sample presents a complete push notification integration supporting: @@ -10,7 +9,7 @@ let readme = - Display push notifications when app is already open - PushNotificationsServiceExtension: - - Rich push notification support (image, gif, audio, video) + - Rich push notification support (image, GIF, audio, video) - PushNotificationsContentExtension: - Braze Push Story implementation diff --git a/Package.swift b/Package.swift index ff36d2b198..0a3c9ad2f2 100644 --- a/Package.swift +++ b/Package.swift @@ -26,8 +26,8 @@ let package = Package( targets: [ .binaryTarget( name: "BrazeKit", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.1.0/BrazeKit.zip", - checksum: "714560e8157832e72c1378a66d3a7700a9ceb3de8da0f30b80dfcc24df980049" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.2.0/BrazeKit.zip", + checksum: "a30dc6397032ebe25319d8a8bdcee7917408228a8b5d071527b9c53250c2ecef" ), .target( name: "BrazeKitResources", @@ -42,18 +42,18 @@ let package = Package( ), .binaryTarget( name: "BrazeLocation", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.1.0/BrazeLocation.zip", - checksum: "1c97c98bde84ce28ef398ad5a738bba874cfe76b7d149480db234ff6099381c1" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.2.0/BrazeLocation.zip", + checksum: "d90083adcf53527c6fadd4b912de4cd0273eb5bb42b0fa55c735ba73f2fa3bbc" ), .binaryTarget( name: "BrazeNotificationService", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.1.0/BrazeNotificationService.zip", - checksum: "75eae3528ae3b0c4a520b4f93195e68989c84487e3a8fe4a9cd0fd67bf49b743" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.2.0/BrazeNotificationService.zip", + checksum: "3245f9128ce38d2d86a24e65695427fc55414d5e09bee531028c8e768424cbac" ), .binaryTarget( name: "BrazePushStory", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.1.0/BrazePushStory.zip", - checksum: "f41bd5d00391c914609e67a2119c47bcbbb02de1573f56bb152dcd0755b7406d" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.2.0/BrazePushStory.zip", + checksum: "d332868baa454e27c172919349746c662511d79b97a12a98e46b946d7ab138c6" ), ] ) diff --git a/README.md b/README.md index ea5540e0fb..ec310b7cc2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ The following features are planned for development. To request new Swift SDK fea | Feature | Estimated Release | |---|---| -| Content Cards | August, 2022 | | tvOS | September, 2022 | | No-code Push Primers, Events, and Attributes | September, 2022 | | Objective-C Migration Library | October, 2022 | diff --git a/Sources/BrazeUI/BrazeUIMocks.swift b/Sources/BrazeUI/BrazeUIMocks.swift index 254b818377..f06437f706 100644 --- a/Sources/BrazeUI/BrazeUIMocks.swift +++ b/Sources/BrazeUI/BrazeUIMocks.swift @@ -17,6 +17,7 @@ import UIKit public static func mockImage( width: CGFloat, height: CGFloat, + text: String? = nil, textSize: CGFloat? = nil, textColor: UIColor = .white, backgroundColor: UIColor = .systemBlue @@ -25,6 +26,7 @@ import UIKit let textSize = textSize ?? min(floor(height / 6), floor(width / 12)) let lineWidth = max(width / 50, 8) let cornerLength = width / 20 + let text = text ?? "\(Int(width))x\(Int(height))" // Draw image to png data let format = UIGraphicsImageRendererFormat.default() @@ -64,15 +66,15 @@ import UIKit let style = NSMutableParagraphStyle() style.alignment = .center style.minimumLineHeight = frame.height / 2 + textSize / 2 - let text = NSAttributedString( - string: "\(Int(width))x\(Int(height))", + let attributedText = NSAttributedString( + string: text, attributes: [ .font: font, .foregroundColor: textColor, .paragraphStyle: style, ] ) - text.draw(in: frame) + attributedText.draw(in: frame) } // Write to temporary cache @@ -82,7 +84,7 @@ import UIKit appropriateFor: nil, create: false ) - let imageUrl = cacheUrl.appendingPathComponent("\(Int(width))x\(Int(height)).png") + let imageUrl = cacheUrl.appendingPathComponent("\(text).png") try! data.write(to: imageUrl) return imageUrl diff --git a/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIBannerCell.swift b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIBannerCell.swift new file mode 100644 index 0000000000..5191307944 --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIBannerCell.swift @@ -0,0 +1,83 @@ +import BrazeKit +import UIKit + +extension BrazeContentCardUI { + + /// The Content Card cell which displays Banner cards. + open class BannerCell: ImageCell { + + /// The type identifier. + public static let identifier = "BrazeContentCardUI.BannerCell" + + // MARK: - Initialization + + /// Initializes the content card cell passing `style` and `reuseIdentifier` to the `super` + /// implementation. + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + container.addSubview(contentImageView) + container.bringSubviewToFront(pinIndicator) + container.bringSubviewToFront(unviewedIndicator) + + installInternalConstraints() + applyAttributes() + } + + /// Does not support interface-builder / storyboards. + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Layout + + open override func installInternalConstraints() { + super.installInternalConstraints() + contentImageView.anchors.edges.pin() + } + + // MARK: - Card Update + + /// Updates the cell with the passed banner content card. + /// - Parameters: + /// - card: The content card to display. + /// - imageLoad: The current image load state. + open func set(card: Braze.ContentCard.Banner, imageLoad: AsyncImageView.ImageLoad?) { + if case .success = imageLoad { + } else { + // Set the image view aspect ratio using the content card's value only if the image isn't + // loaded yet. When the image is loaded, the image view uses the actual aspect ratio + // automatically + contentImageView.aspectRatio = card.imageAspectRatio ?? 1.0 + } + contentImageView.imageLoad = imageLoad + + pinIndicator.isHidden = !card.pinned + unviewedIndicator.isHidden = card.viewed + + highlightable = card.clickAction != .none + } + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + + import SwiftUI + + struct BannerCell_Previews: PreviewProvider { + static let cards: [Braze.ContentCard] = [ + .banner(.mockPinned), + .banner(.mockUnviewed), + .banner(.mockViewed), + ] + static var previews: some View { + BrazeContentCardUI.ViewController(initialCards: cards) + .preview() + } + } + +#endif diff --git a/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUICaptionedImageCell.swift b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUICaptionedImageCell.swift new file mode 100644 index 0000000000..b47a6ae47d --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUICaptionedImageCell.swift @@ -0,0 +1,119 @@ +import BrazeKit +import UIKit + +extension BrazeContentCardUI { + + /// The Content Card cell which displays Captioned Image cards. + open class CaptionedImageCell: ImageCell { + + /// The type identifier. + public static let identifier = "BrazeContentCardUI.CaptionedImageCell" + + // MARK: - Initialization + + /// Initializes the content card cell passing `style` and `reuseIdentifier` to the `super` + /// implementation. + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + container.addSubview(contentImageView) + + let textStack = TextStack() + self.textStack = textStack + container.addSubview(textStack) + + container.bringSubviewToFront(pinIndicator) + container.bringSubviewToFront(unviewedIndicator) + + installInternalConstraints() + applyAttributes() + } + + /// Does not support interface-builder / storyboards. + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// The image height constraint. Unset until ``installInternalConstraints()`` is executed. + open var captionedImageHeight: NSLayoutConstraint! + + /// The text container edges constraints. Unset until ``installInternalConstraints()`` is + /// executed. + open var textContainerContraints: [NSLayoutConstraint]! + + open override func installInternalConstraints() { + super.installInternalConstraints() + + contentImageView.anchors.top.pin() + contentImageView.anchors.edges.pin(axis: .horizontal) + + textContainerContraints = + Constraints { + textStack?.anchors.edges.pin(axis: .horizontal) + textStack?.anchors.top.equal(contentImageView.anchors.bottom) + textStack?.anchors.bottom.pin() + }.constraints + } + + // MARK: - Card Update + + /// Updates the cell with the passed captioned image content card. + /// - Parameters: + /// - card: The content card to display. + /// - imageLoad: The current image load state. + open func set(card: Braze.ContentCard.CaptionedImage, imageLoad: AsyncImageView.ImageLoad?) { + if case .success = imageLoad { + } else { + contentImageView.aspectRatio = card.imageAspectRatio ?? 1.0 + } + contentImageView.imageLoad = imageLoad + + textStack?.titleLabel.text = card.title + textStack?.descriptionLabel.text = card.description + textStack?.domainLabel.text = card.domain + textStack?.domainHidden = card.domain == nil || card.domain == "" + + pinIndicator.isHidden = !card.pinned + unviewedIndicator.isHidden = card.viewed + + highlightable = card.clickAction != .none + } + + open override func applyAttributes() { + super.applyAttributes() + + let padding = attributes.padding + textContainerContraints[0].constant = padding.left + textContainerContraints[1].constant = -padding.right + textContainerContraints[2].constant = padding.top + textContainerContraints[3].constant = -padding.bottom + } + + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + + import SwiftUI + + struct CaptionedImageCell_Previews: PreviewProvider { + static let cards: [Braze.ContentCard] = [ + .captionedImage(.mockPinned), + .captionedImage(.mockUnviewed), + .captionedImage(.mockViewed), + .captionedImage(.mockDomain), + .captionedImage(.mockShort), + .captionedImage(.mockLong), + .captionedImage(.mockExtraLong), + ] + static var previews: some View { + BrazeContentCardUI.ViewController(initialCards: cards) + .preview() + } + } + +#endif diff --git a/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUICell.swift b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUICell.swift new file mode 100644 index 0000000000..1973761434 --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUICell.swift @@ -0,0 +1,381 @@ +import UIKit + +extension BrazeContentCardUI { + + /// The base Content Card cell. + /// + /// This class implements the common appearance and logic needed for displaying content cards. It + /// **does not support** displaying content cards directly. Use one of the available subclass to + /// do so. + open class Cell: UITableViewCell { + + // MARK: - Attributes + + /// The attributes supported by content card cell. + /// + /// Attributes allows customizing the cells. + public struct Attributes: Equatable { + + /// The spacing around the content view. + public var margin: Margin = .layoutMargins( + UIEdgeInsets(top: 16, left: 0, bottom: 16, right: 0) + ) + + /// The spacing around the content's view content. + public var padding: UIEdgeInsets = UIEdgeInsets(top: 20, left: 25, bottom: 25, right: 25) + + /// The maximum width. + public var maxWidth: Double? = nil + + /// The background color. + public var backgroundColor: UIColor = .brazeCellBackgroundColor + + /// The border color. + public var borderColor: UIColor = .brazeCellBorderColor + + /// The border width. + public var borderWidth: Double = 1 + + /// The corner radius. + public var cornerRadius: Double = 3 + + /// The corner curve. + @available(iOS 13.0, *) + public var cornerCurve: CALayerCornerCurve { + get { CALayerCornerCurve(rawValue: _cornerCurve) } + set { _cornerCurve = newValue.rawValue } + } + var _cornerCurve: String = "circular" + + /// The shadow. + public var shadow: Shadow? = .contentCard + + /// The color for the highlighted / selected state. + /// + /// Disable the highlighted state by setting this value to `UIColor.clear`. + public var highlightColor: UIColor = .brazeCellHighlightColor + + /// The pin indicator color, use the cell's `tintColor` when nil. + /// + /// Requires ``pinIndicatorImage`` to be rendered as a template image using + /// [`withRenderingMode(_:)`](https://apple.co/3LMOgfZ). + public var pinIndicatorColor: UIColor? + + /// The pin indicator image, use the default pin indicator image when nil. + public var pinIndicatorImage: UIImage? + + /// The unviewed indicator color, use the cell's `tintColor` when nil. + public var unviewedIndicatorColor: UIColor? + + /// The unviewed indicator height. + public var unviewedIndicatorHeight: Double = 8 + + /// The title's font. + public var titleFont: UIFont = .preferredFont(textStyle: .callout, weight: .bold) + + /// The title's color. + public var titleColor: UIColor = .brazeLabel + + /// The description's font. + public var descriptionFont: UIFont = .preferredFont(textStyle: .footnote, weight: .regular) + + /// The description's color. + public var descriptionColor: UIColor = .brazeLabel + + /// The domain's font. + public var domainFont: UIFont = .preferredFont(textStyle: .footnote, weight: .medium) + + /// The domain's color, use the cell's `tintColor` when nil. + public var domainColor: UIColor? + + /// The size for the image in ``BrazeContentCardUI/ClassicImageCell``. + public var classicImageSize: CGSize = CGSize(width: 57.5, height: 57.5) + + /// The corner radius for the image in ``BrazeContentCardUI/ClassicImageCell``. + public var classicImageCornerRadius: Double = 3 + + /// The spacing between the image and the text in ``BrazeContentCardUI/ClassicImageCell``. + public var classicImageTextSpacing: Double = 12 + + /// The default attributes. + public static let defaults = Self() + } + + /// The margin type + public enum Margin: Equatable { + + /// Use the `layoutMarginsGuide`. + case layoutMargins(UIEdgeInsets) + + /// Use the edges of the cells. + case edges(UIEdgeInsets) + + /// Margin insets. + public var insets: UIEdgeInsets { + switch self { + case .layoutMargins(let insets), .edges(let insets): + return insets + } + } + } + + /// The cell attributes. See ``Attributes-swift.struct``. + open var attributes: Attributes = .init() { + didSet { + guard attributes != oldValue else { return } + applyAttributes() + } + } + + /// Apply the current attributes. + open func applyAttributes() { + // Margin + switch attributes.margin { + case .layoutMargins(let insets): + layoutContainerEdges[0].constant = insets.left + layoutContainerEdges[1].constant = -insets.right + NSLayoutConstraint.deactivate(edgesContainerEdges) + NSLayoutConstraint.activate(layoutContainerEdges) + case .edges(let insets): + edgesContainerEdges[0].constant = insets.left + edgesContainerEdges[1].constant = -insets.right + NSLayoutConstraint.deactivate(layoutContainerEdges) + NSLayoutConstraint.activate(edgesContainerEdges) + } + if containerVerticalEdges[0].constant != attributes.margin.insets.top { + containerVerticalEdges[0].constant = attributes.margin.insets.top + } + if containerVerticalEdges[1].constant != attributes.margin.insets.bottom { + containerVerticalEdges[1].constant = -attributes.margin.insets.bottom + } + + if let maxWidth = attributes.maxWidth { + containerMaxWidth.constant = maxWidth + NSLayoutConstraint.activate([containerMaxWidth]) + } else { + NSLayoutConstraint.deactivate([containerMaxWidth]) + } + + // Background color + containerBackground.backgroundColor = attributes.backgroundColor + + // Border + containerBackground.layer.borderColor = + attributes.borderColor.brazeResolvedColor(with: traitCollection).cgColor + containerBackground.layer.borderWidth = attributes.borderWidth + + // Corners + containerBackground.layer.cornerRadius = attributes.cornerRadius + container.layer.cornerRadius = attributes.cornerRadius + if #available(iOS 13.0, *) { + containerBackground.layer.cornerCurve = attributes.cornerCurve + container.layer.cornerCurve = attributes.cornerCurve + } + + // Shadow + (containerBackground as? ShadowView)?.shadow = attributes.shadow + + // Pin indicator + pinIndicator.tintColor = attributes.pinIndicatorColor ?? tintColor + pinIndicator.image = attributes.pinIndicatorImage ?? pinIndicator.image + + // Unviewed indicator + unviewedIndicator.backgroundColor = attributes.unviewedIndicatorColor ?? tintColor + unviewedIndicatorHeight.constant = attributes.unviewedIndicatorHeight + + // Text customization (if implemented by subclass) + textStack?.titleLabel.font = attributes.titleFont + textStack?.titleLabel.textColor = attributes.titleColor + textStack?.descriptionLabel.font = attributes.descriptionFont + textStack?.descriptionLabel.textColor = attributes.descriptionColor + textStack?.domainLabel.font = attributes.domainFont + textStack?.domainLabel.textColor = attributes.domainColor ?? tintColor + } + + // MARK: - Views + + /// The pin indicator image view. + open lazy var pinIndicator: UIImageView = { + let image = UIImage( + named: "ContentCard/pin", + in: resourcesBundle, + compatibleWith: traitCollection + )? + .withRenderingMode(.alwaysTemplate) + .imageFlippedForRightToLeftLayoutDirection() + let view = UIImageView(image: image) + return view + }() + + /// The unviewed indicator view. + open lazy var unviewedIndicator: UIView = { + let view = UIView() + view.backgroundColor = self.tintColor + return view + }() + + /// The container view. + open lazy var container: UIView = { + let view = UIView() + view.addSubview(pinIndicator) + view.addSubview(unviewedIndicator) + view.layer.cornerRadius = 3 + view.layer.masksToBounds = true + return view + }() + + /// The container background view, containing the container. + open lazy var containerBackground: UIView = { + let view = ShadowView(.contentCard) + view.backgroundColor = .brazeCellBackgroundColor + view.layer.cornerRadius = 3 + view.layer.borderColor = .brazeCellBorderColor(traitCollection) + view.layer.borderWidth = 1 + view.addSubview(container) + return view + }() + + /// The optional text stack. + open var textStack: TextStack? + + /// The viewed state, hides / displays the unviewed indicator. + open var viewed: Bool { + get { unviewedIndicator.isHidden } + set { unviewedIndicator.isHidden = newValue } + } + + // MARK: - Init + + /// Initializes the content card cell passing `style` and `reuseIdentifier` to the `super` + /// implementation. + /// + /// This class implements the common appearance and logic needed for displaying content cards. + /// The base ``BrazeContentCardUI/Cell`` class **does not support** displaying content cards + /// directly. Use one of the available subclass to do so. + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + contentView.addSubview(containerBackground) + + selectionStyle = .none + backgroundColor = .clear + } + + /// Does not support interface-builder / storyboards. + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Layout + + /// The unviewed indicator height constraint. Unset until ``installInternalConstraints()`` is + /// executed. + open var unviewedIndicatorHeight: NSLayoutConstraint! + + /// The horizontal constraints to the content view layout margin guide. Unset until + /// ``installInternalConstraints()`` is executed. + open var layoutContainerEdges: [NSLayoutConstraint]! + + /// The horizontal constraints to the content view edges. Unset until + /// ``installInternalConstraints()`` is executed. + open var edgesContainerEdges: [NSLayoutConstraint]! + + /// The vertical constraints to the content view edges. Unset until + /// ``installInternalConstraints()`` is executed. + open var containerVerticalEdges: [NSLayoutConstraint]! + + /// The max width constraint. Unset until ``installInternalConstraints()`` is + /// executed. + open var containerMaxWidth: NSLayoutConstraint! + + /// Install the internal auto-layout constraints. + /// + /// The base ``BrazeContentCardUI/Cell`` class never calls this method. It is the subclass + /// responsibility to optionally override the implementation and call it when appropriate, + /// usually after all views have been added to the view hierarchy, at the end of the + /// initializer. + open func installInternalConstraints() { + Constraints(activate: false) { + layoutContainerEdges = containerBackground.anchors.edges.pin( + to: contentView.layoutMarginsGuide, + axis: .horizontal + ) + edgesContainerEdges = containerBackground.anchors.edges.pin(axis: .horizontal) + containerMaxWidth = containerBackground.anchors.width.lessThanOrEqual(100) + } + // Lower priority to allow the max width constraint to work + (layoutContainerEdges + edgesContainerEdges).forEach { $0.priority = .required - 1 } + + Constraints { + container.anchors.edges.pin() + + containerBackground.anchors.centerX.align() + containerVerticalEdges = containerBackground.anchors.edges.pin(axis: .vertical) + + unviewedIndicatorHeight = unviewedIndicator.anchors.height.equal( + attributes.unviewedIndicatorHeight + ) + unviewedIndicator.anchors.edges.pin(axis: .horizontal) + unviewedIndicator.anchors.bottom.pin() + + pinIndicator.anchors.edges.pin(alignment: .topTrailing) + } + } + + // MARK: - Theme + + /// See [`traitCollectionDidChange(_:)`](https://developer.apple.com/documentation/uikit/uitraitenvironment/1623516-traitcollectiondidchange) + open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + applyAttributes() + } + + /// See [`tintColorDidChange()`](https://developer.apple.com/documentation/uikit/uiview/1622620-tintcolordidchange) + open override func tintColorDidChange() { + super.tintColorDidChange() + applyAttributes() + } + + // MARK: - User Interactions + + /// Whether the cell can be highlighted or not. By default, content cards without click action + /// are not highlightable. + open var highlightable: Bool = true + + /// The animator driving the highlighted state. + open var highlightedAnimator: UIViewPropertyAnimator? + + /// See [`setHighlighted(_:animated:)`](https://developer.apple.com/documentation/uikit/uitableviewcell/1623280-sethighlighted) + open override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + + highlightedAnimator?.stopAnimation(true) + + guard highlightable else { + containerBackground.backgroundColor = attributes.backgroundColor + return + } + + if highlighted { + self.containerBackground.backgroundColor = self.attributes.highlightColor + } else { + highlightedAnimator = UIViewPropertyAnimator(duration: 0.25, curve: .easeOut) { + self.containerBackground.backgroundColor = self.attributes.backgroundColor + } + highlightedAnimator?.startAnimation() + } + } + + /// Overrides `UIView.hitTest(_:with:)` default implementation to ignore touches outside of the + /// ``container`` view. + /// + /// See [`hitTest(_:with:)`](https://developer.apple.com/documentation/uikit/uiview/1622469-hittest) + open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let view = super.hitTest(point, with: event) + return (view == container || view?.responders.lazy.contains(container) == true) ? view : nil + } + + } + +} diff --git a/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIClassicCell.swift b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIClassicCell.swift new file mode 100644 index 0000000000..8c244dd205 --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIClassicCell.swift @@ -0,0 +1,97 @@ +import BrazeKit +import UIKit + +extension BrazeContentCardUI { + + /// The Content Card cell which displays Classic cards. + open class ClassicCell: Cell { + + /// The type identifier. + public static let identifier = "BrazeContentCardUI.ClassicCell" + + // MARK: - Initialization + + /// Initializes the content card cell passing `style` and `reuseIdentifier` to the `super` + /// implementation. + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + // View hierarchy + let textStack = TextStack() + self.textStack = textStack + container.addSubview(textStack) + + installInternalConstraints() + applyAttributes() + } + + /// Does not support interface-builder / storyboards. + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Layout + + /// The text stack edges constraints. Unset until ``installInternalConstraints()`` is + /// executed. + open var textStackContraints: [NSLayoutConstraint]! + + open override func installInternalConstraints() { + super.installInternalConstraints() + textStackContraints = textStack?.anchors.edges.pin() + } + + // MARK: - Card Update + + /// Updates the cell with the passed classic content card. + /// - Parameter card: The content card to display. + open func set(card: Braze.ContentCard.Classic) { + textStack?.titleLabel.text = card.title + textStack?.descriptionLabel.text = card.description + textStack?.domainLabel.text = card.domain + textStack?.domainHidden = card.domain == nil || card.domain == "" + + pinIndicator.isHidden = !card.pinned + unviewedIndicator.isHidden = card.viewed + + highlightable = card.clickAction != .none + } + + open override func applyAttributes() { + super.applyAttributes() + + let padding = attributes.padding + textStackContraints[0].constant = padding.left + textStackContraints[1].constant = -padding.right + textStackContraints[2].constant = padding.top + textStackContraints[3].constant = -padding.bottom + } + + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + + import SwiftUI + + struct ClassicCell_Previews: PreviewProvider { + static let cards: [Braze.ContentCard] = [ + .classic(.mockPinned), + .classic(.mockUnviewed), + .classic(.mockViewed), + .classic(.mockDomain), + .classic(.mockShort), + .classic(.mockLong), + .classic(.mockExtraLong), + ] + static var previews: some View { + BrazeContentCardUI.ViewController(initialCards: cards) + .preview() + } + } + +#endif diff --git a/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIClassicImageCell.swift b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIClassicImageCell.swift new file mode 100644 index 0000000000..3a281906aa --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIClassicImageCell.swift @@ -0,0 +1,129 @@ +import BrazeKit +import UIKit + +extension BrazeContentCardUI { + + /// The Content Card cell which displays Classic Image cards. + open class ClassicImageCell: ImageCell { + + /// The type identifier. + public static let identifier = "BrazeContentCardUI.ClassicImageCell" + + // MARK: - Views + + /// The horizontal stack view containing the image and the text stack. + open var cardStack: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.spacing = 12 + stack.alignment = .top + return stack + }() + + // MARK: - Initialization + + /// Initializes the content card cell passing `style` and `reuseIdentifier` to the `super` + /// implementation. + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + // View hierachy + let textStack = TextStack() + self.textStack = textStack + + contentImageView.fixedAspectRatio = 1.0 + contentImageView.activityIndicator.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + + cardStack.addArrangedSubview(contentImageView) + cardStack.addArrangedSubview(textStack) + container.addSubview(cardStack) + + installInternalConstraints() + applyAttributes() + } + + /// Does not support interface-builder / storyboards. + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Layout + + /// The horizontal stack edges constraints. Unset until ``installInternalConstraints()`` is + /// executed. + open var cardStackConstraints: [NSLayoutConstraint]! + + /// The classic image size constraints. Unset until ``installInternalConstraints()`` is + /// executed. + open var classicImageSize: [NSLayoutConstraint]! + + open override func installInternalConstraints() { + super.installInternalConstraints() + classicImageSize = contentImageView.anchors.size.equal(CGSize(width: 10, height: 10)) + cardStackConstraints = cardStack.anchors.edges.pin() + } + + // MARK: - Card Update + + /// Updates the cell with the passed classic image content card. + /// - Parameters: + /// - card: The content card to display. + /// - imageLoad: The current image load state. + open func set(card: Braze.ContentCard.ClassicImage, imageLoad: AsyncImageView.ImageLoad?) { + contentImageView.imageLoad = imageLoad + + textStack?.titleLabel.text = card.title + textStack?.descriptionLabel.text = card.description + textStack?.domainLabel.text = card.domain + textStack?.domainHidden = card.domain == nil || card.domain == "" + + pinIndicator.isHidden = !card.pinned + unviewedIndicator.isHidden = card.viewed + + highlightable = card.clickAction != .none + } + + open override func applyAttributes() { + super.applyAttributes() + + let padding = attributes.padding + cardStackConstraints[0].constant = padding.left + cardStackConstraints[1].constant = -padding.right + cardStackConstraints[2].constant = padding.top + cardStackConstraints[3].constant = -padding.bottom + + contentImageView.imageCornerRadius = attributes.classicImageCornerRadius + classicImageSize[0].constant = attributes.classicImageSize.width + classicImageSize[1].constant = attributes.classicImageSize.height + + cardStack.spacing = attributes.classicImageTextSpacing + } + + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + + import SwiftUI + + struct ClassicImageCell_Previews: PreviewProvider { + static let cards: [Braze.ContentCard] = [ + .classicImage(.mockPinned), + .classicImage(.mockUnviewed), + .classicImage(.mockViewed), + .classicImage(.mockDomain), + .classicImage(.mockShort), + .classicImage(.mockLong), + .classicImage(.mockExtraLong), + ] + static var previews: some View { + BrazeContentCardUI.ViewController(initialCards: cards) + .preview() + } + } + +#endif diff --git a/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIControlCell.swift b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIControlCell.swift new file mode 100644 index 0000000000..ce5ae5078b --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIControlCell.swift @@ -0,0 +1,26 @@ +import UIKit + +extension BrazeContentCardUI { + + /// The Content Card cell used for Control cards. + open class ControlCell: UITableViewCell { + + /// The type identifier. + public static let identifier = "BrazeContentCardUI.ControlCell" + + /// Initializes the content card cell passing `style` and `reuseIdentifier` to the `super` + /// implementation. + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + backgroundColor = .clear + } + + /// Does not support interface-builder / storyboards. + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + } + +} diff --git a/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIImageCell.swift b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIImageCell.swift new file mode 100644 index 0000000000..cc18248604 --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUIImageCell.swift @@ -0,0 +1,18 @@ +import UIKit + +extension BrazeContentCardUI { + + /// A Content Card cell subclass providing a pre-configured image view. + open class ImageCell: Cell { + + /// The image view used to display the content card image. + open var contentImageView: AsyncImageView = { + let imageView = AsyncImageView() + imageView.backgroundColor = .brazeCellImageBackgroundColor + imageView.tintColor = .brazeRetryButtonColor + return imageView + }() + + } + +} diff --git a/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUITextStack.swift b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUITextStack.swift new file mode 100644 index 0000000000..2be14c1383 --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/Cells/ContentCardUITextStack.swift @@ -0,0 +1,72 @@ +import UIKit + +extension BrazeContentCardUI { + + /// A `UIStackView` subclass used to display a content card's title, description and optionally + /// domain aligned on the leading vertical axis. + open class TextStack: UIStackView { + + /// Whether the domain is hidden or not. + open var domainHidden: Bool { + get { domainLabel.isHidden } + set { domainLabel.isHidden = newValue } + } + + // MARK: - Views + + /// The title label. + open var titleLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.adjustsFontForContentSizeCategory = true + return label + }() + + /// The description label. + open var descriptionLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.adjustsFontForContentSizeCategory = true + return label + }() + + /// The domain label. + open lazy var domainLabel: UILabel = { + let label = UILabel() + label.textColor = self.tintColor + label.adjustsFontForContentSizeCategory = true + return label + }() + + // MARK: - Init + + /// Creates and returns a text stack used to display a content card's title, description and + /// optionally domain aligned on the leading vertical axis. + /// - Parameter frame: The view frame. + public override init(frame: CGRect) { + super.init(frame: frame) + + axis = .vertical + alignment = .leading + + addArrangedSubview(titleLabel) + addArrangedSubview(descriptionLabel) + addArrangedSubview(domainLabel) + + if #available(iOS 11.0, *) { + spacing = UIStackView.spacingUseSystem + } else { + spacing = 10 + } + } + + /// Does not support interface-builder / storyboards. + @available(*, unavailable) + public required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + +} diff --git a/Sources/BrazeUI/ContentCardUI/ContentCardMocks.swift b/Sources/BrazeUI/ContentCardUI/ContentCardMocks.swift new file mode 100644 index 0000000000..e369e6e1ef --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/ContentCardMocks.swift @@ -0,0 +1,267 @@ +import BrazeKit +import Foundation +import UIKit + +#if DEBUG + + // MARK: - Classic + + extension Braze.ContentCard.Classic { + + public static let mockPinned = Self( + data: .init(viewed: true, pinned: true), + title: "Pinned", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockUnviewed = Self( + data: .init(), + title: "Unviewed", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockViewed = Self( + data: .init(viewed: true), + title: "Viewed", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockDomain = Self( + data: .init(viewed: true), + title: "Domain", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake.", + domain: "domain.com" + ) + + public static let mockShort = Self( + data: .init(viewed: true), + title: "Short Text", + description: "abc" + ) + + public static let mockLong = Self( + data: .init(viewed: true), + title: + "Long Text | Cupcake ipsum dolor sit amet cake brownie powder. Pudding chupa chups toffee liquorice biscuit pastry dragée marshmallow bonbon. Gummi bears liquorice gingerbread donut pudding liquorice.", + description: + """ + Cupcake ipsum dolor sit amet cake brownie powder. Pudding chupa chups toffee liquorice biscuit pastry dragée marshmallow bonbon. Gummi bears liquorice gingerbread donut pudding liquorice. + + Tiramisu bear claw icing chocolate cake brownie halvah caramels. Pastry tiramisu toffee cookie gummies carrot cake candy canes fruitcake. Pastry marshmallow cake sweet roll cake muffin. + + Tootsie roll pudding danish brownie topping bonbon tart. Dragée gingerbread lollipop toffee candy jujubes. Wafer gummi bears jelly-o soufflé cake cheesecake apple pie caramels chupa chups. Wafer jelly beans jelly-o caramels chocolate cake chocolate cake cake candy. + """ + ) + + public static let mockExtraLong = Self( + data: .init(viewed: true), + title: + "Extra Long Text | Cupcake ipsum dolor sit amet carrot cake tootsie roll jujubes. Powder tiramisu jelly beans cake bear claw chocolate oat cake cotton candy tart. Jelly lemon drops brownie chocolate bar brownie cotton candy. Brownie chupa chups jelly halvah jelly beans shortbread soufflé. Cheesecake oat cake oat cake apple pie cheesecake candy canes jelly-o. Candy canes cupcake muffin cotton candy lollipop pudding. Icing jelly-o shortbread icing icing. Jelly-o lollipop lemon drops cake tiramisu cheesecake. Croissant gummi bears powder gingerbread cotton candy.", + description: + """ + Cupcake ipsum dolor sit amet carrot cake tootsie roll jujubes. Powder tiramisu jelly beans cake bear claw chocolate oat cake cotton candy tart. Jelly lemon drops brownie chocolate bar brownie cotton candy. Brownie chupa chups jelly halvah jelly beans shortbread soufflé. Cheesecake oat cake oat cake apple pie cheesecake candy canes jelly-o. Candy canes cupcake muffin cotton candy lollipop pudding. Icing jelly-o shortbread icing icing. Jelly-o lollipop lemon drops cake tiramisu cheesecake. Croissant gummi bears powder gingerbread cotton candy. + + Halvah halvah lollipop cookie ice cream brownie macaroon shortbread. Pie sesame snaps chocolate chocolate cake cupcake danish. Jelly beans lemon drops icing tootsie roll marzipan croissant soufflé soufflé. Chupa chups marzipan apple pie pastry gingerbread. Jelly beans oat cake cotton candy chupa chups topping chocolate bar liquorice. Topping tiramisu lollipop gingerbread cake icing lemon drops candy canes sweet. Danish marzipan marshmallow cake pudding chocolate cake lollipop cake marshmallow. Chocolate fruitcake halvah oat cake tart powder cupcake marshmallow marshmallow. + + Brownie tootsie roll candy canes sweet bonbon. Gingerbread sugar plum sweet tart jelly-o. Sesame snaps tootsie roll soufflé lollipop jelly sugar plum halvah pastry wafer. Pie halvah tiramisu lemon drops jelly apple pie. Pastry shortbread carrot cake oat cake shortbread lollipop. Pudding sweet roll fruitcake marzipan halvah. + + Chocolate bar marshmallow bonbon cake cheesecake chocolate bar dessert muffin. Sugar plum dragée sesame snaps bear claw jelly fruitcake. Powder lollipop cupcake wafer marzipan donut pudding jujubes fruitcake. Gingerbread dragée dessert chupa chups cheesecake tart. Chocolate bar pudding cake cookie gummi bears icing. Wafer apple pie icing sesame snaps candy. Marzipan muffin icing marzipan cupcake bear claw cake. Caramels oat cake tiramisu cake candy jelly. Apple pie ice cream caramels candy canes carrot cake candy bear claw. Brownie sweet roll oat cake icing bear claw wafer. + + Sweet roll cookie jelly-o soufflé sugar plum bear claw dragée candy canes gummies. Cake carrot cake brownie donut chocolate gummies brownie cotton candy. Lollipop biscuit candy halvah chocolate cake biscuit sweet. Chocolate bar danish gummies pastry icing. Carrot cake pudding jujubes dragée toffee fruitcake toffee. Gingerbread caramels chupa chups jelly-o jujubes cookie sesame snaps. + """ + ) + } + + // MARK: - ClassicImage + + extension Braze.ContentCard.ClassicImage { + + public static let mockPinned = Self( + data: .init(viewed: true, pinned: true), + image: .mockImage(width: 100, height: 100, text: "100", textSize: 20), + title: "Pinned", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockUnviewed = Self( + data: .init(), + image: .mockImage(width: 100, height: 100, text: "100", textSize: 20), + title: "Unviewed", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockViewed = Self( + data: .init(viewed: true), + image: .mockImage(width: 100, height: 100, text: "100", textSize: 20), + title: "Viewed", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockDomain = Self( + data: .init(viewed: true), + image: .mockImage(width: 100, height: 100, text: "100", textSize: 20), + title: "Domain", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake.", + domain: "domain.com" + ) + + public static let mockShort = Self( + data: .init(viewed: true), + image: .mockImage(width: 100, height: 100, text: "100", textSize: 20), + title: "Short Text", + description: "abc" + ) + + public static let mockLong = Self( + data: .init(viewed: true), + image: .mockImage(width: 100, height: 100, text: "100", textSize: 20), + title: + "Long Text | Cupcake ipsum dolor sit amet cake brownie powder. Pudding chupa chups toffee liquorice biscuit pastry dragée marshmallow bonbon. Gummi bears liquorice gingerbread donut pudding liquorice.", + description: + """ + Cupcake ipsum dolor sit amet cake brownie powder. Pudding chupa chups toffee liquorice biscuit pastry dragée marshmallow bonbon. Gummi bears liquorice gingerbread donut pudding liquorice. + + Tiramisu bear claw icing chocolate cake brownie halvah caramels. Pastry tiramisu toffee cookie gummies carrot cake candy canes fruitcake. Pastry marshmallow cake sweet roll cake muffin. + + Tootsie roll pudding danish brownie topping bonbon tart. Dragée gingerbread lollipop toffee candy jujubes. Wafer gummi bears jelly-o soufflé cake cheesecake apple pie caramels chupa chups. Wafer jelly beans jelly-o caramels chocolate cake chocolate cake cake candy. + """ + ) + + public static let mockExtraLong = Self( + data: .init(viewed: true), + image: .mockImage(width: 100, height: 100, text: "100", textSize: 20), + title: + "Extra Long Text | Cupcake ipsum dolor sit amet carrot cake tootsie roll jujubes. Powder tiramisu jelly beans cake bear claw chocolate oat cake cotton candy tart. Jelly lemon drops brownie chocolate bar brownie cotton candy. Brownie chupa chups jelly halvah jelly beans shortbread soufflé. Cheesecake oat cake oat cake apple pie cheesecake candy canes jelly-o. Candy canes cupcake muffin cotton candy lollipop pudding. Icing jelly-o shortbread icing icing. Jelly-o lollipop lemon drops cake tiramisu cheesecake. Croissant gummi bears powder gingerbread cotton candy.", + description: + """ + Cupcake ipsum dolor sit amet carrot cake tootsie roll jujubes. Powder tiramisu jelly beans cake bear claw chocolate oat cake cotton candy tart. Jelly lemon drops brownie chocolate bar brownie cotton candy. Brownie chupa chups jelly halvah jelly beans shortbread soufflé. Cheesecake oat cake oat cake apple pie cheesecake candy canes jelly-o. Candy canes cupcake muffin cotton candy lollipop pudding. Icing jelly-o shortbread icing icing. Jelly-o lollipop lemon drops cake tiramisu cheesecake. Croissant gummi bears powder gingerbread cotton candy. + + Halvah halvah lollipop cookie ice cream brownie macaroon shortbread. Pie sesame snaps chocolate chocolate cake cupcake danish. Jelly beans lemon drops icing tootsie roll marzipan croissant soufflé soufflé. Chupa chups marzipan apple pie pastry gingerbread. Jelly beans oat cake cotton candy chupa chups topping chocolate bar liquorice. Topping tiramisu lollipop gingerbread cake icing lemon drops candy canes sweet. Danish marzipan marshmallow cake pudding chocolate cake lollipop cake marshmallow. Chocolate fruitcake halvah oat cake tart powder cupcake marshmallow marshmallow. + + Brownie tootsie roll candy canes sweet bonbon. Gingerbread sugar plum sweet tart jelly-o. Sesame snaps tootsie roll soufflé lollipop jelly sugar plum halvah pastry wafer. Pie halvah tiramisu lemon drops jelly apple pie. Pastry shortbread carrot cake oat cake shortbread lollipop. Pudding sweet roll fruitcake marzipan halvah. + + Chocolate bar marshmallow bonbon cake cheesecake chocolate bar dessert muffin. Sugar plum dragée sesame snaps bear claw jelly fruitcake. Powder lollipop cupcake wafer marzipan donut pudding jujubes fruitcake. Gingerbread dragée dessert chupa chups cheesecake tart. Chocolate bar pudding cake cookie gummi bears icing. Wafer apple pie icing sesame snaps candy. Marzipan muffin icing marzipan cupcake bear claw cake. Caramels oat cake tiramisu cake candy jelly. Apple pie ice cream caramels candy canes carrot cake candy bear claw. Brownie sweet roll oat cake icing bear claw wafer. + + Sweet roll cookie jelly-o soufflé sugar plum bear claw dragée candy canes gummies. Cake carrot cake brownie donut chocolate gummies brownie cotton candy. Lollipop biscuit candy halvah chocolate cake biscuit sweet. Chocolate bar danish gummies pastry icing. Carrot cake pudding jujubes dragée toffee fruitcake toffee. Gingerbread caramels chupa chups jelly-o jujubes cookie sesame snaps. + """ + ) + + } + + // MARK: - Banner + + extension Braze.ContentCard.Banner { + + private static let backgroundColor = UIColor(red: 0.38, green: 0.64, blue: 0.74, alpha: 1.00) + + public static let mockPinned = Self( + data: .init(viewed: true, pinned: true), + image: .mockImage( + width: 400, + height: 200, + text: "Pinned", + backgroundColor: backgroundColor + ) + ) + + public static let mockUnviewed = Self( + data: .init(), + image: .mockImage( + width: 400, + height: 200, + text: "Unviewed", + backgroundColor: backgroundColor + ) + ) + + public static let mockViewed = Self( + data: .init(viewed: true), + image: .mockImage( + width: 400, + height: 200, + text: "Viewed", + backgroundColor: backgroundColor + ) + ) + + } + + // MARK: - CaptionedImage + + extension Braze.ContentCard.CaptionedImage { + + private static let backgroundColor = UIColor(red: 0.38, green: 0.64, blue: 0.74, alpha: 1.00) + + public static let mockPinned = Self( + data: .init(viewed: true, pinned: true), + image: .mockImage(width: 600, height: 400, backgroundColor: backgroundColor), + title: "Pinned", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockUnviewed = Self( + data: .init(), + image: .mockImage(width: 600, height: 400, backgroundColor: backgroundColor), + title: "Unviewed", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockViewed = Self( + data: .init(viewed: true), + image: .mockImage(width: 600, height: 400, backgroundColor: backgroundColor), + title: "Viewed", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockDomain = Self( + data: .init(viewed: true), + image: .mockImage(width: 600, height: 400, backgroundColor: backgroundColor), + title: "Domain", + description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake.", + domain: "domain.com" + ) + + public static let mockShort = Self( + data: .init(viewed: true), + image: .mockImage(width: 600, height: 400, backgroundColor: backgroundColor), + title: "Short Text", + description: "abc" + ) + + public static let mockLong = Self( + data: .init(viewed: true), + image: .mockImage(width: 600, height: 400, backgroundColor: backgroundColor), + title: + "Long Text | Cupcake ipsum dolor sit amet cake brownie powder. Pudding chupa chups toffee liquorice biscuit pastry dragée marshmallow bonbon. Gummi bears liquorice gingerbread donut pudding liquorice.", + description: + """ + Cupcake ipsum dolor sit amet cake brownie powder. Pudding chupa chups toffee liquorice biscuit pastry dragée marshmallow bonbon. Gummi bears liquorice gingerbread donut pudding liquorice. + + Tiramisu bear claw icing chocolate cake brownie halvah caramels. Pastry tiramisu toffee cookie gummies carrot cake candy canes fruitcake. Pastry marshmallow cake sweet roll cake muffin. + + Tootsie roll pudding danish brownie topping bonbon tart. Dragée gingerbread lollipop toffee candy jujubes. Wafer gummi bears jelly-o soufflé cake cheesecake apple pie caramels chupa chups. Wafer jelly beans jelly-o caramels chocolate cake chocolate cake cake candy. + """ + ) + + public static let mockExtraLong = Self( + data: .init(viewed: true), + image: .mockImage(width: 600, height: 400, backgroundColor: backgroundColor), + title: + "Extra Long Text | Cupcake ipsum dolor sit amet carrot cake tootsie roll jujubes. Powder tiramisu jelly beans cake bear claw chocolate oat cake cotton candy tart. Jelly lemon drops brownie chocolate bar brownie cotton candy. Brownie chupa chups jelly halvah jelly beans shortbread soufflé. Cheesecake oat cake oat cake apple pie cheesecake candy canes jelly-o. Candy canes cupcake muffin cotton candy lollipop pudding. Icing jelly-o shortbread icing icing. Jelly-o lollipop lemon drops cake tiramisu cheesecake. Croissant gummi bears powder gingerbread cotton candy.", + description: + """ + Cupcake ipsum dolor sit amet carrot cake tootsie roll jujubes. Powder tiramisu jelly beans cake bear claw chocolate oat cake cotton candy tart. Jelly lemon drops brownie chocolate bar brownie cotton candy. Brownie chupa chups jelly halvah jelly beans shortbread soufflé. Cheesecake oat cake oat cake apple pie cheesecake candy canes jelly-o. Candy canes cupcake muffin cotton candy lollipop pudding. Icing jelly-o shortbread icing icing. Jelly-o lollipop lemon drops cake tiramisu cheesecake. Croissant gummi bears powder gingerbread cotton candy. + + Halvah halvah lollipop cookie ice cream brownie macaroon shortbread. Pie sesame snaps chocolate chocolate cake cupcake danish. Jelly beans lemon drops icing tootsie roll marzipan croissant soufflé soufflé. Chupa chups marzipan apple pie pastry gingerbread. Jelly beans oat cake cotton candy chupa chups topping chocolate bar liquorice. Topping tiramisu lollipop gingerbread cake icing lemon drops candy canes sweet. Danish marzipan marshmallow cake pudding chocolate cake lollipop cake marshmallow. Chocolate fruitcake halvah oat cake tart powder cupcake marshmallow marshmallow. + + Brownie tootsie roll candy canes sweet bonbon. Gingerbread sugar plum sweet tart jelly-o. Sesame snaps tootsie roll soufflé lollipop jelly sugar plum halvah pastry wafer. Pie halvah tiramisu lemon drops jelly apple pie. Pastry shortbread carrot cake oat cake shortbread lollipop. Pudding sweet roll fruitcake marzipan halvah. + + Chocolate bar marshmallow bonbon cake cheesecake chocolate bar dessert muffin. Sugar plum dragée sesame snaps bear claw jelly fruitcake. Powder lollipop cupcake wafer marzipan donut pudding jujubes fruitcake. Gingerbread dragée dessert chupa chups cheesecake tart. Chocolate bar pudding cake cookie gummi bears icing. Wafer apple pie icing sesame snaps candy. Marzipan muffin icing marzipan cupcake bear claw cake. Caramels oat cake tiramisu cake candy jelly. Apple pie ice cream caramels candy canes carrot cake candy bear claw. Brownie sweet roll oat cake icing bear claw wafer. + + Sweet roll cookie jelly-o soufflé sugar plum bear claw dragée candy canes gummies. Cake carrot cake brownie donut chocolate gummies brownie cotton candy. Lollipop biscuit candy halvah chocolate cake biscuit sweet. Chocolate bar danish gummies pastry icing. Carrot cake pudding jujubes dragée toffee fruitcake toffee. Gingerbread caramels chupa chups jelly-o jujubes cookie sesame snaps. + """ + ) + + } + +#endif diff --git a/Sources/BrazeUI/ContentCardUI/ContentCardUI.swift b/Sources/BrazeUI/ContentCardUI/ContentCardUI.swift new file mode 100644 index 0000000000..56e0aa390c --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/ContentCardUI.swift @@ -0,0 +1,28 @@ +import BrazeKit +import UIKit + +/// The Braze provided Content Cards UI namespace. +public enum BrazeContentCardUI {} + +extension Braze.ContentCard { + + /// The cell identifier used by ``BrazeContentCardUI/ViewController`` to display the content + /// cards. + public var cellIdentifier: String { + switch self { + case .classic: + return BrazeContentCardUI.ClassicCell.identifier + case .classicImage: + return BrazeContentCardUI.ClassicImageCell.identifier + case .banner: + return BrazeContentCardUI.BannerCell.identifier + case .captionedImage: + return BrazeContentCardUI.CaptionedImageCell.identifier + case .control: + return BrazeContentCardUI.ControlCell.identifier + @unknown default: + return "BrazeContentCardUI.unknown" + } + } + +} diff --git a/Sources/BrazeUI/ContentCardUI/ContentCardUIModalViewController.swift b/Sources/BrazeUI/ContentCardUI/ContentCardUIModalViewController.swift new file mode 100644 index 0000000000..4ce3ccfb59 --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/ContentCardUIModalViewController.swift @@ -0,0 +1,87 @@ +import BrazeKit +import UIKit + +extension BrazeContentCardUI { + + /// Wraps ``ViewController`` in a `UINavigationController` with a _Done_ button. Use this class + /// for presenting the content cards modally. + open class ModalViewController: UINavigationController { + + // MARK: - Properties + + public let viewController: ViewController + + // MARK: - Initialization + + /// Creates and return a table view controller displaying the latest content cards fetched by + /// the Braze SDK. + /// + /// - Parameters: + /// - braze: The Braze instance. + /// - attributes: An attributes struct allowing customization of the table view controller + /// and its cells. + /// - title: The navigation bar title (default: `""`) + public init( + braze: Braze, + attributes: ViewController.Attributes = .defaults, + title: String = "" + ) { + viewController = .init(braze: braze, attributes: attributes) + viewController.title = title + super.init(rootViewController: viewController) + } + + /// Creates and returns a table view controller able to display content cards. + /// + /// For most use cases, prefer using ``init(braze:attributes:)`` instead. + /// + /// - Parameters: + /// - initialCards: The initial Content Cards displayed. + /// - refresh: An optional closure implementing the refresh logic. `nil` disables pull to + /// refresh. + /// - subscribe: An optional closure implementing the subscription to new cards logic. `nil` + /// disables automatic updates. + /// - attributes: An attributes struct allowing customization of the table view controller + /// and its cells. + /// - title: The navigation bar title (default: `""`) + public init( + initialCards: [Braze.ContentCard], + refresh: ((@escaping (Result<[Braze.ContentCard], Error>) -> Void) -> Void)? = nil, + subscribe: ((@escaping ([Braze.ContentCard]) -> Void) -> Braze.Cancellable)? = nil, + attributes: ViewController.Attributes = .defaults, + title: String = "" + ) { + viewController = .init( + initialCards: initialCards, + refresh: refresh, + subscribe: subscribe, + attributes: attributes + ) + viewController.title = title + super.init(rootViewController: viewController) + } + + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - LifeCycle + + open override func viewDidLoad() { + super.viewDidLoad() + viewController.navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(dismissModal) + ) + } + + @objc + open func dismissModal() { + dismiss(animated: true) + } + + } + +} diff --git a/Sources/BrazeUI/ContentCardUI/ContentCardUISwiftUI.swift b/Sources/BrazeUI/ContentCardUI/ContentCardUISwiftUI.swift new file mode 100644 index 0000000000..6b78e36a53 --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/ContentCardUISwiftUI.swift @@ -0,0 +1,94 @@ +// Disable SwiftUI features for `arch(arm)` (armv7) as it doesn't have SwiftUI symbols: +// See: https://archive.ph/eMbWT (FB7431741) +#if canImport(SwiftUI) && !arch(arm) + + import BrazeKit + import SwiftUI + import UIKit + + /// A SwiftUI view which displays Braze Content Cards. + @available(iOS 13.0, *) + public struct ContentCardsView: UIViewControllerRepresentable { + + /// The attributes supported by the view. + /// + /// Attributes allows customizing the view and its associated cells. + public typealias Attributes = BrazeContentCardUI.ViewController.Attributes + + weak var braze: Braze? + let shouldProcess: (Braze.ContentCard.ClickAction, Braze.ContentCard) -> Bool + let attributes: Attributes + + /// Creates and returns a view displaying the latest content cards fetched by the Braze SDK. + /// - Parameters: + /// - braze: The Braze instance. + /// - shouldProcess: Whether Braze should process the Content Card click action. See + /// ``BrazeContentCardUIViewControllerDelegate/contentCard(_:shouldProcess:card:)-6v08v`` + /// for more details (default: returns true). + /// - 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 + ) { + self.braze = braze + self.shouldProcess = shouldProcess + self.attributes = attributes + } + + public func makeUIViewController(context: Context) -> BrazeContentCardUI.ViewController { + guard let braze = braze else { return .init(initialCards: []) } + let viewController = BrazeContentCardUI.ViewController(braze: braze, attributes: attributes) + viewController.delegate = context.coordinator + return viewController + } + + public func updateUIViewController( + _ viewController: BrazeContentCardUI.ViewController, + context: Context + ) { + viewController.delegate = context.coordinator + viewController.attributes = attributes + if let braze = braze, braze !== context.coordinator.braze { + context.coordinator.braze = braze + viewController.updateWithBraze(braze) + } + } + + public func makeCoordinator() -> Coordinator { + Coordinator(braze: braze, shouldProcess: shouldProcess) + } + + } + + @available(iOS 13.0, *) + extension ContentCardsView { + + public class Coordinator: BrazeContentCardUIViewControllerDelegate { + + weak var braze: Braze? + var shouldProcess: (Braze.ContentCard.ClickAction, Braze.ContentCard) -> Bool + + init( + braze: Braze?, + shouldProcess: @escaping (Braze.ContentCard.ClickAction, Braze.ContentCard) -> Bool + ) { + self.braze = braze + self.shouldProcess = shouldProcess + } + + public func contentCard( + _ controller: BrazeContentCardUI.ViewController, + shouldProcess clickAction: Braze.ContentCard.ClickAction, + card: Braze.ContentCard + ) -> Bool { + shouldProcess(clickAction, card) + } + + } + + } + +#endif diff --git a/Sources/BrazeUI/ContentCardUI/ContentCardUIViewController.swift b/Sources/BrazeUI/ContentCardUI/ContentCardUIViewController.swift new file mode 100644 index 0000000000..06b51737bb --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/ContentCardUIViewController.swift @@ -0,0 +1,659 @@ +import BrazeKit +import UIKit + +extension BrazeContentCardUI { + + /// A view controller which displays Braze Content Cards. + open class ViewController: UITableViewController, UITableViewDataSourcePrefetching { + + /// The content cards currently displayed. + open var cards: [Braze.ContentCard] = [] { + didSet { emptyStateLabel.isHidden = cards.isEmpty == false } + } + + /// The delegate notified of the content cards UI lifecycle. + open weak var delegate: BrazeContentCardUIViewControllerDelegate? + + /// The internal refresh implementation. Use ``refreshCards()`` instead. + var refresh: ((@escaping (Result<[Braze.ContentCard], Error>) -> Void) -> Void)? + + /// The internal cards updates subscription implementation. + var subscribe: ((@escaping ([Braze.ContentCard]) -> Void) -> Braze.Cancellable)? + + /// The date the content cards where last updated. + var lastUpdate: Date? + + /// The current cards update subscription. + private var subscription: Braze.Cancellable? + + /// The current asynchronous image loading operations. + var imageLoads: [URL: AsyncImageView.ImageLoad] = [:] + + // MARK: - Attributes + + /// The attributes supported by the view controller. + /// + /// Attributes allows customizing the view controller and its associated cells. + public struct Attributes { + + /// The _cell identifier_ to _cell class_ map. + /// + /// You can replace the _cell class_ by one of your own subclass to customize the appearance + /// of the content card. + /// + /// See ``cellAttributes`` to customize cells without subclassing. + public var cells: [String: UITableViewCell.Type] = [ + ClassicCell.identifier: ClassicCell.self, + ClassicImageCell.identifier: ClassicImageCell.self, + BannerCell.identifier: BannerCell.self, + CaptionedImageCell.identifier: CaptionedImageCell.self, + ControlCell.identifier: ControlCell.self, + ] + + /// The cell attributes customizing the content card appearance. + public var cellAttributes: BrazeContentCardUI.Cell.Attributes = .defaults + + /// The background color. + public var backgroundColor: UIColor = .brazeTableViewBackgroundColor + + /// Flag specifying whether the pull to refresh gesture is enabled. + public var pullToRefresh: Bool = true + + /// Timeout for refreshing content cards when the list is initially presented (default: `60` + /// seconds). + /// + /// When content cards were last updated more than `automaticRefreshTimeout` seconds ago, + /// the view controller automatically requests a refresh when it is displayed. + /// + /// Sets this value to `nil` to disable the automatic refresh. + public var automaticRefreshTimeout: TimeInterval? = 60 + + /// Flag specifying whether the controller should subscribe to card updates. + public var automaticUpdates: Bool = true + + /// 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 } + + /// The message displayed when there is no content cards available. + public var emptyStateMessage: String = localize( + "braze.content-cards.no-card.text", + for: .contentCard + ) + + /// The empty state message font. + public var emptyStateMessageFont: UIFont = .preferredFont(forTextStyle: .body) + + /// The empty state message color. + public var emptyStateMessageColor: UIColor = .brazeLabel + + /// The default attributes. + public static let defaults = Self() + } + + /// The attributes customizing the table view and its cells. + open var attributes: Attributes { + didSet { applyAttributes() } + } + + /// Apply the current attributes to the UI, setup subscriptions. + open func applyAttributes() { + // Cells + attributes.cells.forEach { tableView.register($1, forCellReuseIdentifier: $0) } + + // Background + view.backgroundColor = attributes.backgroundColor + + // Pull to refresh + if attributes.pullToRefresh, refresh != nil { + refreshControl = UIRefreshControl() + refreshControl?.layer.zPosition = -1 + refreshControl?.addTarget(self, action: #selector(refreshCards), for: .valueChanged) + } else { + refreshControl = nil + } + + // Automatic updates + if attributes.automaticUpdates, view.window != nil { + subscription = subscribeToUpdates() + } else { + subscription?.cancel() + } + + // EmptyStateLabel + emptyStateLabel.text = attributes.emptyStateMessage + emptyStateLabel.font = attributes.emptyStateMessageFont + emptyStateLabel.textColor = attributes.emptyStateMessageColor + } + + // MARK: Initialization + + /// Creates and return a table view controller displaying the latest content cards fetched by + /// the Braze SDK. + /// + /// - Parameters: + /// - 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) { + self.init( + initialCards: braze.contentCards.cards, + refresh: { [weak braze] fulfill in + braze?.contentCards.requestRefresh { result in fulfill(result) } + }, + subscribe: { [weak braze] update in + braze?.contentCards.subscribeToUpdates(update) ?? .empty + }, + lastUpdate: braze.contentCards.lastUpdate, + attributes: attributes + ) + } + + /// Creates and return a table view controller able to display content cards. For most use + /// cases, prefer using ``init(braze:attributes:)`` instead. + /// + /// - Parameters: + /// - initialCards: The initial Content Cards displayed. + /// - refresh: An optional closure implementing the refresh logic. `nil` disables pull to + /// refresh. + /// - subscribe: An optional closure implementing the subscription to new cards logic. `nil` + /// disables automatic updates. + /// - attributes: An attributes struct allowing customization of the table view controller + /// and its cells. + public init( + initialCards: [Braze.ContentCard], + refresh: ((@escaping (Result<[Braze.ContentCard], Error>) -> Void) -> Void)? = nil, + subscribe: ((@escaping ([Braze.ContentCard]) -> Void) -> Braze.Cancellable)? = nil, + lastUpdate: Date? = nil, + attributes: Attributes = .defaults + ) { + self.cards = attributes.transform(initialCards) + self.refresh = refresh + self.subscribe = subscribe + self.lastUpdate = lastUpdate + self.attributes = attributes + super.init(style: .plain) + } + + /// Does not support interface-builder / storyboards. + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - LifeCycle + + open override func viewDidLoad() { + super.viewDidLoad() + setupTableView() + setupEmptyState() + applyAttributes() + } + + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if attributes.automaticUpdates { + subscription = subscribeToUpdates() + } + + if let timeout = attributes.automaticRefreshTimeout, + Date().timeIntervalSince(lastUpdate ?? .distantPast) > timeout + { + refreshCards() + } + + impressionTracker.start() + } + + open override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + subscription?.cancel() + + impressionTracker.stop() + } + + // MARK: - Layout + + open override func viewWillTransition( + to size: CGSize, + with coordinator: UIViewControllerTransitionCoordinator + ) { + super.viewWillTransition(to: size, with: coordinator) + coordinator.animate { _ in self.tableView.reloadData() } + } + + // MARK: - UITableView + + /// Setup the table view + open func setupTableView() { + + // Cells + // - iOS 10 requires explicitly setting `estimatedRowHeight` to enable self-sizing cells + if #available(iOS 11.0, *) { + } else { + tableView.estimatedRowHeight = 200 + } + + // - iOS 10 & 11 `cellLayoutMarginsFollowReadableWidth` default to true + tableView.cellLayoutMarginsFollowReadableWidth = false + + // Appearance + tableView.separatorStyle = .none + + // Prefetch + tableView.prefetchDataSource = self + } + + /// Retrieves the content card for the provided index path. + open func card(at indexPath: IndexPath) -> Braze.ContentCard? { + guard cards.indices.contains(indexPath.row) else { return nil } + return cards[indexPath.row] + } + + /// Retrieves the content card with the provided id. + open func card(id: String) -> Braze.ContentCard? { + cards.first(where: { $0.id == id }) + } + + /// The index path for the provided content card. + open func indexPath(for card: Braze.ContentCard) -> IndexPath? { + guard let index = cards.firstIndex(where: { $0.id == card.id }) else { return nil } + return IndexPath(row: index, section: 0) + } + + /// Dequeue and setup a matching cell for the provided content card at index path. + /// - Parameters: + /// - card: The content card. + /// - indexPath: The index path. + /// - tableView: The table view displaying the cell. + /// - Returns: The matching cell for the provided content card. + open func cell( + for card: Braze.ContentCard, + at indexPath: IndexPath, + in tableView: UITableView + ) -> UITableViewCell { + func dequeue(as: T.Type) -> T { + tableView.dequeueReusableCell(withIdentifier: card.cellIdentifier, for: indexPath) as! T + } + let attributes = self.attributes.cellAttributes + let imageLoad = loadImage(card: card) + let retry = { [weak self] in + guard let self = self, + let imageLoad = self.loadImage(card: card, retry: true) + else { return } + self.updateImageCell(card: card, imageLoad: imageLoad) + } + + switch card { + case .classic(let classic): + let cell = dequeue(as: ClassicCell.self) + cell.attributes = attributes + cell.set(card: classic) + return cell + case .classicImage(let classicImage): + let cell = dequeue(as: ClassicImageCell.self) + cell.attributes = attributes + cell.set(card: classicImage, imageLoad: imageLoad) + return cell + case .banner(let banner): + let cell = dequeue(as: BannerCell.self) + cell.contentImageView.retry = retry + cell.attributes = attributes + cell.set(card: banner, imageLoad: imageLoad) + return cell + case .captionedImage(let captionedImage): + let cell = dequeue(as: CaptionedImageCell.self) + cell.contentImageView.retry = retry + cell.attributes = attributes + cell.set(card: captionedImage, imageLoad: imageLoad) + return cell + case .control: + return dequeue(as: ControlCell.self) + @unknown default: + return dequeue(as: ControlCell.self) + } + } + + // MARK: DataSource + + open override func tableView( + _ tableView: UITableView, + numberOfRowsInSection section: Int + ) -> Int { + cards.count + } + + open override func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + cell(for: card(at: indexPath)!, at: indexPath, in: tableView) + } + + open override func tableView( + _ tableView: UITableView, + willDisplay cell: UITableViewCell, + forRowAt indexPath: IndexPath + ) { + guard let card = card(at: indexPath) else { return } + updateImageCell(cell: cell, imageLoad: loadImage(card: card)) + } + + // MARK: Delegate + + open override func tableView( + _ tableView: UITableView, + heightForRowAt indexPath: IndexPath + ) -> CGFloat { + if case .control = card(at: indexPath) { + // We must return at least one pixel otherwise the table view has trouble reporting the + // control cells properly in `tableView(_:willDisplay:forRowAt:)` and + // `tableView(_:didEndDisplaying:forRowAt:)` + return 1.0 / UIScreen.main.scale + } + return UITableView.automaticDimension + } + + open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + if let card = card(at: indexPath) { + cardClicked(card, indexPath: indexPath) + } + } + + open override func tableView( + _ tableView: UITableView, + canEditRowAt indexPath: IndexPath + ) -> Bool { + card(at: indexPath)?.dismissible ?? false + } + + open override func tableView( + _ tableView: UITableView, + commit editingStyle: UITableViewCell.EditingStyle, + forRowAt indexPath: IndexPath + ) { + guard let card = card(at: indexPath), editingStyle == .delete else { return } + cardDismissed(card, indexPath: indexPath) + } + + // MARK: Prefetching + + open func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + indexPaths + .compactMap(card(at:)) + .forEach { loadImage(card: $0) } + } + + open func tableView( + _ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath] + ) { + indexPaths + .compactMap(card(at:)) + .forEach(cancelLoadImage(card:)) + } + + // MARK: - Subscribe + + /// Subscribes to content card updates + /// - Returns: A cancellable that must be retained for the duration of the subscription. + open func subscribeToUpdates() -> Braze.Cancellable? { + subscribe? { [weak self] cards in + guard let self = self else { return } + self.lastUpdate = Date() + self.cards = self.attributes.transform(cards) + self.tableView.reloadData() + } + } + + // MARK: - Refresh + + /// Refresh the content cards. + @objc open func refreshCards() { + refreshControl?.beginRefreshing() + refresh? { [weak self] result in + guard let self = self else { return } + if case .success(let cards) = result { + self.lastUpdate = Date() + self.cards = self.attributes.transform(cards) + self.tableView.reloadData() + } + self.refreshControl?.endRefreshing() + } + } + + // MARK: - Content Cards operations + + /// Log the content card dismissed event and remove the cell. + /// - Parameters: + /// - card: The card dismissed. + /// - indexPath: The index path for the cell to remove. + open func cardDismissed(_ card: Braze.ContentCard, indexPath: IndexPath) { + if card.removed { return } + card.context?.logDismissed() + cancelLoadImage(card: card) + cards.remove(at: indexPath.row) + tableView.deleteRows(at: [indexPath], with: .automatic) + } + + /// Log the content card impression event. + /// - Parameters: + /// - card: The visible card. + /// - indexPath: The index path for the cell. + open func cardImpression(_ card: Braze.ContentCard, indexPath: IndexPath) { + if card.viewed { return } + card.context?.logImpression() + + // We just update the local data model here, the unviewed indicator is updated separately via + // `cardViewed(_:indexPath:cell:)` + cards[indexPath.row].viewed = true + } + + /// Mark the card as viewed, hide the cell's unviewed indicator. + /// - Parameters: + /// - card: The card viewed. + /// - indexPath: The index path for the cell. + /// - cell: The cell to update, it is automatically retrieved from the table view when `nil`. + open func cardViewed(_ card: Braze.ContentCard, indexPath: IndexPath, cell: Cell? = nil) { + cards[indexPath.row].viewed = true + if let cell = cell ?? tableView.cellForRow(at: indexPath) as? Cell { + cell.viewed = true + } + } + + /// Log the content card click event and process the click action. + /// - Parameters: + /// - card: The card clicked. + /// - indexPath: The index path for the cell to update. + open func cardClicked(_ card: Braze.ContentCard, indexPath: IndexPath) { + cardViewed(card, indexPath: indexPath) + + card.context?.logClick() + cards[indexPath.row].clicked = true + + guard let clickAction = card.clickAction else { + return + } + let process = delegate?.contentCard(self, shouldProcess: clickAction, card: card) ?? true + if process { + card.context?.processClickAction(clickAction) + } + } + + // MARK: - Visibility Tracking + + lazy var impressionTracker: VisibilityTracker = .init( + interval: 0.1, + visibleIdentifiers: { [weak self] in + guard let self = self, + let visibleIndexPaths = self.tableView.indexPathsForVisibleRows + else { return [] } + let ids = visibleIndexPaths.compactMap(self.card(at:)).map(\.id) + return ids + }, + visibleForInterval: { [weak self] id in + guard let self = self, + let card = self.card(id: id), + let indexPath = self.indexPath(for: card) + else { return } + self.cardImpression(card, indexPath: indexPath) + }, + exitVisible: { [weak self] indexPath, afterInterval in + guard let self = self, + let card = self.card(id: indexPath), + let indexPath = self.indexPath(for: card), + afterInterval + else { return } + self.cardViewed(card, indexPath: indexPath) + } + ) + + // MARK: - Image Loading + + @discardableResult + func loadImage(card: Braze.ContentCard, retry: Bool = false) -> AsyncImageView.ImageLoad? { + guard let imageURL = card.imageURL else { return nil } + + // Local images + if imageURL.isFileURL { + if imageLoads[imageURL] == nil { + imageLoads[imageURL] = .success(imageURL, imageSize(url: imageURL) ?? .zero) + } + return imageLoads[imageURL] + } + + // Remote images + guard let contextLoadImage = card.context?.loadImage else { return nil } + func load(_ imageURL: URL) -> AsyncImageView.ImageLoad { + let imageLoad: AsyncImageView.ImageLoad = + imageURL.isFileURL + ? .success(imageURL, imageSize(url: imageURL) ?? .zero) + : .loading( + contextLoadImage { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let localURL): + let size = imageSize(url: localURL) ?? .zero + self.imageLoads[imageURL] = .success(localURL, size) + self.updateImageCell(card: card, imageLoad: .success(localURL, size)) + case .failure(let error): + self.imageLoads[imageURL] = .failed(error) + self.updateImageCell(card: card, imageLoad: .failed(error)) + } + } + ) + imageLoads[imageURL] = imageLoad + return imageLoad + } + + switch imageLoads[imageURL] { + case .none: + return load(imageURL) + case .failed where retry: + return load(imageURL) + case .some(let imageLoad): + return imageLoad + } + } + + func cancelLoadImage(card: Braze.ContentCard) { + guard let imageURL = card.imageURL, + let imageLoad = imageLoads[imageURL], + case .loading(let cancellable) = imageLoad + else { return } + imageLoads[imageURL] = nil + cancellable.cancel() + } + + func updateImageCell(card: Braze.ContentCard, imageLoad: AsyncImageView.ImageLoad?) { + guard let indexPath = indexPath(for: card), + let cell = tableView.cellForRow(at: indexPath) as? BrazeContentCardUI.ImageCell + else { return } + updateImageCell(cell: cell, imageLoad: imageLoad) + } + + func updateImageCell(cell: UITableViewCell, imageLoad: AsyncImageView.ImageLoad?) { + guard let cell = cell as? BrazeContentCardUI.ImageCell else { return } + UIView.performWithoutAnimation { + tableView.beginUpdates() + cell.contentImageView.imageLoad = imageLoad + tableView.endUpdates() + } + } + + // MARK: - Empty State + + /// The label displayed when no content card is available. + open lazy var emptyStateLabel: UILabel = { + let label = UILabel() + label.adjustsFontSizeToFitWidth = true + label.adjustsFontForContentSizeCategory = true + label.textAlignment = .center + label.numberOfLines = 0 + label.isHidden = cards.isEmpty == false + return label + }() + + /// Setup the empty state view. + open func setupEmptyState() { + view.addSubview(emptyStateLabel) + emptyStateLabel.anchors.edges.pin(to: view.readableContentGuide) + } + + // MARK: - Braze Update + + /// Updates the view controller and attach it to a new `Braze` instance. + /// - Parameter braze: The new `Braze` instance. + open func updateWithBraze(_ braze: Braze) { + cards = attributes.transform(braze.contentCards.cards) + refresh = { [weak braze] fulfill in + braze?.contentCards.requestRefresh { result in fulfill(result) } + } + subscribe = { [weak braze] update in + braze?.contentCards.subscribeToUpdates(update) ?? .empty + } + lastUpdate = braze.contentCards.lastUpdate + applyAttributes() + imageLoads = [:] + tableView.reloadData() + } + + } +} + +// MARK: - Previews + +#if UI_PREVIEWS + import SwiftUI + + @available(iOS 13.0, *) + struct BrazeContentCardUIViewController_Previews: PreviewProvider { + + static let cards: [Braze.ContentCard] = [ + .classic(.mockDomain), + .classicImage(.mockUnviewed), + .banner(.mockPinned), + .captionedImage(.mockShort), + ] + + public static var previews: some View { + BrazeContentCardUI.ViewController(initialCards: cards) + .preview() + } + } + + // Previews don't seem to like accessing `BrazeContentCardUI` types directly from the + // `ViewController`. We alias them here just so that previews find them during compilation. + extension BrazeContentCardUI.ViewController { + public typealias Cell = BrazeContentCardUI.Cell + public typealias ClassicCell = BrazeContentCardUI.ClassicCell + public typealias ClassicImageCell = BrazeContentCardUI.ClassicImageCell + public typealias BannerCell = BrazeContentCardUI.BannerCell + public typealias CaptionedImageCell = BrazeContentCardUI.CaptionedImageCell + public typealias ControlCell = BrazeContentCardUI.ControlCell + } + +#endif diff --git a/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate.swift b/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate.swift new file mode 100644 index 0000000000..6b3a5b8eff --- /dev/null +++ b/Sources/BrazeUI/ContentCardUI/ContentCardUIViewControllerDelegate.swift @@ -0,0 +1,41 @@ +import BrazeKit + +/// Methods for reacting to the content card UI lifecycle. +public protocol BrazeContentCardUIViewControllerDelegate: AnyObject { + + /// Defines whether Braze should process the Content Card click action. + /// + /// If the method returns `true` (default return value), Braze processes the click action. + /// + /// - Important: When this method returns `true` and the click action is a url, Braze will + /// **still** execute the `BrazeDelegate.braze(_:shouldOpenURL:)` delegate method + /// offering a last opportunity to modify or replace Braze url handling behavior. + /// If your implementation only needs access to the `url`, use + /// `BrazeDelegate.braze(_:shouldOpenURL:)` instead. + /// + /// - Parameters: + /// - controller: The view controller containing the Content Card. + /// - clickAction: The click action. + /// - card: The Content Card. + /// - Returns: `true` to let Braze process the click action, `false` otherwise + func contentCard( + _ controller: BrazeContentCardUI.ViewController, + shouldProcess clickAction: Braze.ContentCard.ClickAction, + card: Braze.ContentCard + ) -> Bool + +} + +// MARK: - Default implementation + +extension BrazeContentCardUIViewControllerDelegate { + + public func contentCard( + _ controller: BrazeContentCardUI.ViewController, + shouldProcess clickAction: Braze.ContentCard.ClickAction, + card: Braze.ContentCard + ) -> Bool { + true + } + +} diff --git a/Sources/BrazeUI/Dependencies/AsyncImageView.swift b/Sources/BrazeUI/Dependencies/AsyncImageView.swift new file mode 100644 index 0000000000..2ed98ed22f --- /dev/null +++ b/Sources/BrazeUI/Dependencies/AsyncImageView.swift @@ -0,0 +1,165 @@ +import BrazeKit +import UIKit + +/// An image view able to represent multiple state of an asynchronous image loading operation. +open class AsyncImageView: UIView { + + /// The image loading operation states + public enum ImageLoad { + case loading(Braze.Cancellable) + case failed(Error) + case success(URL, CGSize) + } + + public override init(frame: CGRect) { + super.init(frame: frame) + + // ImageView + addSubview(imageView) + imageView.anchors.edges.pin() + + // Activity indicator + addSubview(activityIndicator) + activityIndicator.anchors.center.align() + + // Retry button + addSubview(retryButton) + retryButton.anchors.center.align() + + // Aspect ratio + aspectRatioConstraint = imageView.anchors.size.height.equal( + anchors.width * (1 / effectiveAspectRatio)) + } + + /// Does not support interface-builder / storyboards. + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Layout + + open override func layoutSubviews() { + super.layoutSubviews() + updateImageCornerRadius() + } + + // MARK: - Aspect Ratio + + public var fixedAspectRatio: Double? { + didSet { updateAspectRatio() } + } + + public var aspectRatio: Double = 1.0 { + didSet { + guard aspectRatio != oldValue, fixedAspectRatio == nil else { return } + updateAspectRatio() + } + } + + private var effectiveAspectRatio: Double { + fixedAspectRatio ?? aspectRatio + } + + private var aspectRatioConstraint: NSLayoutConstraint! + + open func updateAspectRatio() { + aspectRatioConstraint.isActive = false + aspectRatioConstraint = imageView.anchors.height.equal( + anchors.width * (1.0 / effectiveAspectRatio)) + aspectRatioConstraint.isActive = true + } + + // MARK: - Image + + public var imageLoad: ImageLoad? { + didSet { updateImage() } + } + + public let imageView: UIView = { + let view = gifViewProvider.view(nil) + view.contentMode = .scaleAspectFit + return view + }() + + public let activityIndicator: UIActivityIndicatorView = { + let indicator: UIActivityIndicatorView + if #available(iOS 13.0, *) { + indicator = UIActivityIndicatorView(style: .large) + } else { + indicator = UIActivityIndicatorView() + } + indicator.hidesWhenStopped = true + return indicator + }() + + private func updateImage() { + // Reset + activityIndicator.stopAnimating() + gifViewProvider.updateView(imageView, nil) + retryButton.isHidden = true + + switch imageLoad { + case .none: + break + case .loading: + activityIndicator.startAnimating() + case .failed: + retryButton.isHidden = retry == nil + case .success(let url, let size): + aspectRatio = size.width / size.height + gifViewProvider.updateView(imageView, url) + updateImageCornerRadius() + } + } + + // MARK: - Corner Radius + + public var imageCornerRadius: Double? { + didSet { updateImageCornerRadius() } + } + + func updateImageCornerRadius() { + guard let cornerRadius = imageCornerRadius else { + layer.mask = nil + return + } + let width = bounds.width + let height = bounds.height + + let rect: CGRect + if aspectRatio < 1 { + let maskWidth = height * aspectRatio + let maskX = (width - maskWidth) / 2 + rect = CGRect(x: maskX, y: 0, width: maskWidth, height: height) + } else { + let maskHeight = width / aspectRatio + let maskY = (height - maskHeight) / 2 + rect = CGRect(x: 0, y: maskY, width: width, height: maskHeight) + } + + let mask = layer.mask as? CAShapeLayer ?? CAShapeLayer() + mask.path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath + layer.mask = mask + } + + // MARK: - Retry / Error + + public var retry: (() -> Void)? + public lazy var retryButton: UIButton = { + let image = UIImage( + named: "ContentCard/retry", + in: resourcesBundle, + compatibleWith: traitCollection + )? + .withRenderingMode(.alwaysTemplate) + .imageFlippedForRightToLeftLayoutDirection() + + let button = UIButton() + button.setImage(image, for: .normal) + button.addAction { [weak self] in self?.retry?() } + + return button + }() + +} diff --git a/Sources/BrazeUI/Dependencies/GIFViewProvider/GIFViewProvider.swift b/Sources/BrazeUI/Dependencies/GIFViewProvider.swift similarity index 79% rename from Sources/BrazeUI/Dependencies/GIFViewProvider/GIFViewProvider.swift rename to Sources/BrazeUI/Dependencies/GIFViewProvider.swift index 4e61d01b32..49c813597c 100644 --- a/Sources/BrazeUI/Dependencies/GIFViewProvider/GIFViewProvider.swift +++ b/Sources/BrazeUI/Dependencies/GIFViewProvider.swift @@ -1,20 +1,20 @@ import UIKit -/// The gif view provider used for all BrazeUI components. +/// 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. +/// By default, Braze displays animated GIFs as static images. +/// See ``GIFViewProvider-swift.struct`` for details about how to add support for animated GIFs. public var gifViewProvider: GIFViewProvider = .default -/// A type providing methods to create and update views supporting animated gif images. +/// A type providing methods to create and update views supporting animated GIF images. /// -/// Braze does not provide animated gif support out of the box. Support can be added by wrapping a +/// Braze does not provide animated GIF support out of the box. Support can be added by wrapping a /// third party or your own view in an instance of `GIFViewProvider`. /// /// Sample implementations for popular third party libraries are provided in -/// . +/// . /// -/// Adding any of those libraries to your project allows you to enable animated gif support in +/// Adding any of those libraries to your project allows you to enable animated GIF support in /// Braze's UI components. /// For instance, a project including SDWebImage and using the compatible sample code can do: /// ```swift @@ -22,7 +22,7 @@ public var gifViewProvider: GIFViewProvider = .default /// ``` public struct GIFViewProvider { - /// Creates a view able to display static and animated gif images. + /// 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 @@ -33,7 +33,7 @@ public struct GIFViewProvider { /// - url: The local file url for the image. public var updateView: (_ view: UIView, _ url: URL?) -> Void - /// Creates a gif view provider. + /// Creates a GIF view provider. /// - Parameters: /// - view: See ``view``. /// - updateView: See ``updateView``. diff --git a/Sources/BrazeUI/Dependencies/Localize.swift b/Sources/BrazeUI/Dependencies/Localize.swift index d08a988500..75976b8c7f 100644 --- a/Sources/BrazeUI/Dependencies/Localize.swift +++ b/Sources/BrazeUI/Dependencies/Localize.swift @@ -2,7 +2,7 @@ import Foundation enum LocalizationSet: String { case inAppMessage = "InAppMessageLocalizable" - case contentCard + case contentCard = "ContentCardsLocalizable" case newsFeed } @@ -13,5 +13,6 @@ func localize(_ key: String, for localizationSet: LocalizationSet) -> String { return override } - return resourcesBundle?.localizedString(forKey: key, value: nil, table: localizationSet.rawValue) ?? key + return resourcesBundle?.localizedString(forKey: key, value: nil, table: localizationSet.rawValue) + ?? key } diff --git a/Sources/BrazeUI/Dependencies/Shadow.swift b/Sources/BrazeUI/Dependencies/Shadow.swift index 063b021957..0d70f84378 100644 --- a/Sources/BrazeUI/Dependencies/Shadow.swift +++ b/Sources/BrazeUI/Dependencies/Shadow.swift @@ -7,61 +7,26 @@ public struct Shadow: Equatable { public var radius: CGFloat public var opacity: Float - /// Default shadow used by Braze's in-app messages. - public static let `default` = Self( + public init(color: UIColor, offset: CGSize, radius: CGFloat, opacity: Float) { + self.color = color + self.offset = offset + self.radius = radius + self.opacity = opacity + } + + /// Braze's default in-app messages shadow. + public static let inAppMessage = Self( color: .black.withAlphaComponent(0.3), offset: .zero, radius: 4.0, opacity: 1 ) -} - -extension UIView { - - /// Syntactic sugar around the layer's shadow properties. - /// - /// When using this property to set the view's shadow, you can use ``updateShadow()`` to resize - /// the shadow to the view's current size. - var shadow: Shadow? { - get { - guard let color = layer.shadowColor.flatMap(UIColor.init) else { - return nil - } - return .init( - color: color, - offset: layer.shadowOffset, - radius: layer.shadowRadius, - opacity: layer.opacity - ) - } - set { - guard let value = newValue else { - layer.shadowColor = nil - layer.shadowOffset = .zero - layer.shadowRadius = 0 - layer.shadowOpacity = 0 - layer.shadowPath = nil - return - } - layer.shadowColor = value.color.cgColor - layer.shadowOffset = value.offset - layer.shadowRadius = value.radius - layer.shadowOpacity = value.opacity - updateShadow() - } - } - - /// Resizes the layer's shadow path to the view's current size. - func updateShadow() { - guard shadow != nil else { - return - } - let path = UIBezierPath( - roundedRect: bounds, - cornerRadius: layer.cornerRadius - ).cgPath - - layer.shadowPath = path - } + /// Braze's default content card shadow. + public static let contentCard = Self( + color: .brazeCellShadowColor, + offset: CGSize(width: 0, height: 2), + radius: 3, + opacity: 0.5 + ) } diff --git a/Sources/BrazeUI/Dependencies/ShadowView.swift b/Sources/BrazeUI/Dependencies/ShadowView.swift new file mode 100644 index 0000000000..c8a8753ee7 --- /dev/null +++ b/Sources/BrazeUI/Dependencies/ShadowView.swift @@ -0,0 +1,74 @@ +import UIKit + +/// A view rendering an auto-updating shadow. +open class ShadowView: UIView { + + /// The view's shadow. + open var shadow: Shadow? { + didSet { drawShadow() } + } + + open override var bounds: CGRect { + didSet { drawShadow() } + } + + /// Creates and returns a shadow view. + /// - Parameter shadow: The shadow value. + public init(_ shadow: Shadow) { + self.shadow = shadow + + super.init(frame: .zero) + + layer.shouldRasterize = true + layer.rasterizationScale = UIScreen.main.scale + } + + /// Does not support interface-builder / storyboards. + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Use ``init(_:)`` instead. + @available(*, unavailable) + public override init(frame: CGRect) { + super.init(frame: frame) + } + + /// Draws the shadow with the current ``shadow``. + open func drawShadow() { + guard let shadow = shadow else { + layer.shadowOffset = .zero + layer.shadowRadius = 0 + layer.shadowOpacity = 0 + layer.shadowColor = nil + layer.shadowPath = nil + return + } + + // Params + layer.shadowOffset = shadow.offset + layer.shadowRadius = shadow.radius + layer.shadowOpacity = shadow.opacity + + // Color + layer.shadowColor = shadow.color.brazeResolvedColor(with: traitCollection).cgColor + + // Size + let path = UIBezierPath( + roundedRect: bounds, + cornerRadius: layer.cornerRadius + ).cgPath + layer.shadowPath = path + } + + open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + guard #available(iOS 13.0, *), + traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) + else { + return + } + drawShadow() + } + +} diff --git a/Sources/BrazeUI/Dependencies/UIKitExt.swift b/Sources/BrazeUI/Dependencies/UIKitExt.swift index ace9aaa408..912cbe7010 100644 --- a/Sources/BrazeUI/Dependencies/UIKitExt.swift +++ b/Sources/BrazeUI/Dependencies/UIKitExt.swift @@ -100,3 +100,102 @@ extension String { } } + +extension UIColor { + + static var brazeTableViewBackgroundColor: UIColor { + if #available(iOS 13.0, *) { + return .systemGroupedBackground + } else { + return .groupTableViewBackground + } + } + + static var brazeCellBackgroundColor: UIColor { + if #available(iOS 13.0, *) { + return .secondarySystemGroupedBackground + } else { + return .white + } + } + + static var brazeCellBorderColor: UIColor { + if #available(iOS 13.0, *) { + return .systemGray5 + } else { + return UIColor(white: 0.88, alpha: 1) + } + } + + static var brazeCellShadowColor: UIColor { + if #available(iOS 13.0, *) { + return UIColor { traits in + switch traits.userInterfaceStyle { + case .dark: + return .systemGray6 + default: + return .systemGray2 + } + } + } else { + return UIColor(white: 0.7, alpha: 1) + } + } + + static var brazeCellHighlightColor: UIColor { + if #available(iOS 13.0, *) { + return .systemGray4 + } else { + return UIColor(red: 0.82, green: 0.82, blue: 0.84, alpha: 1) + } + } + + static var brazeCellImageBackgroundColor: UIColor { + if #available(iOS 13.0, *) { + return .tertiarySystemGroupedBackground + } else { + return UIColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1.00) + } + } + + static var brazeRetryButtonColor: UIColor { + if #available(iOS 13.0, *) { + return .secondarySystemFill + } else { + return UIColor(red: 0.87, green: 0.87, blue: 0.89, alpha: 1.00) + } + } + + static var brazeLabel: UIColor { + if #available(iOS 13.0, *) { + return .label + } else { + return .black + } + } + +} + +extension UIColor { + + func brazeResolvedColor(with traitCollection: UITraitCollection) -> UIColor { + if #available(iOS 13.0, *) { + return self.resolvedColor(with: traitCollection) + } else { + return self + } + } + +} + +extension CGColor { + + static func brazeCellBorderColor(_ traits: UITraitCollection) -> CGColor { + if #available(iOS 13.0, *) { + return UIColor.brazeCellBorderColor.resolvedColor(with: traits).cgColor + } else { + return UIColor.brazeCellBorderColor.cgColor + } + } + +} diff --git a/Sources/BrazeUI/Dependencies/VisibilityTracker.swift b/Sources/BrazeUI/Dependencies/VisibilityTracker.swift new file mode 100644 index 0000000000..2de1f49312 --- /dev/null +++ b/Sources/BrazeUI/Dependencies/VisibilityTracker.swift @@ -0,0 +1,113 @@ +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. +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 lazy var displayLink: CADisplayLink = createDisplayLink() + private var identifiersMap: [Identifier: CFTimeInterval] = [:] + private var previousTimestamp: CFTimeInterval = CACurrentMediaTime() + + // MARK: - Initialization + + /// Creates a visibility tracker. + /// - Parameters: + /// - interval: The interval of time after which the `visibleForInterval` closure is called. + /// - visibleIdentifiers: Provide the currently visible identifiers to the visibility tracker. + /// This closure is executed multiple times per second. + /// - visibleForInterval: A closure executed when an identifier has remained visible more than + /// `interval` seconds. + /// - exitVisible: A closure executed when an identifier exits the list of visible identifiers. + /// The second parameter indicates if the identifier has remained visible more + /// 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 } + ) { + self.interval = interval + self.visibleIdentifiers = visibleIdentifiers + self.visibleForInterval = visibleForInterval + self.exitVisible = exitVisible + } + + // MARK: - Actions + + /// Starts tracking the identifiers visibility. + public func start() { + displayLink.invalidate() + displayLink = createDisplayLink() + previousTimestamp = CACurrentMediaTime() + displayLink.add(to: .main, forMode: .common) + } + + /// Stops tracking the identifiers visibility + /// - Parameter setInvisible: Marks the current identifiers as invisible and execute the + /// `exitVisible` closure. + public func stop(setInvisible: Bool = true) { + displayLink.invalidate() + + if setInvisible { + identifiersMap.keys.forEach { exitVisible($0, false) } + identifiersMap = [:] + } + } + + // MARK: - CADisplayLink Tick + + @objc + private func displayLinkTick(_ displayLink: CADisplayLink) { + defer { previousTimestamp = displayLink.timestamp } + + let previous = Set(identifiersMap.keys) + let current = Set(visibleIdentifiers()) + + let insertions = current.subtracting(previous) + let deletions = previous.subtracting(current) + let visibles = current.intersection(previous) + + insertions.forEach { + identifiersMap[$0] = displayLink.timestamp + } + + deletions.forEach { + let afterInterval = identifiersMap[$0] == -1 + identifiersMap[$0] = nil + exitVisible($0, afterInterval) + } + + let visiblesAfterInterval = + identifiersMap + .filter { id, start in + visibles.contains(id) + && start != -1 + && displayLink.timestamp - start > interval + } + .keys + visiblesAfterInterval + .forEach { + identifiersMap[$0] = -1 + visibleForInterval($0) + } + } + + // MARK: - Misc. + + private func createDisplayLink() -> CADisplayLink { + let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkTick)) + if #available(iOS 15.0, *) { + displayLink.preferredFrameRateRange = .init(minimum: 10, maximum: 20, preferred: 15) + } else { + displayLink.preferredFramesPerSecond = 15 + } + return displayLink + } +} diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageMocks.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageMocks.swift index 26a68af912..bc61464c33 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageMocks.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageMocks.swift @@ -12,6 +12,11 @@ import Foundation message: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." ) + public static let mockText = Self( + data: .mockNoClickAction, + message: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + public static let mockShortText = Self( data: .mock, message: "Short" @@ -34,6 +39,11 @@ import Foundation message: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." ) + public static let mockShort = Self( + data: .init(), + message: "abc" + ) + public static let mockLong = Self( data: .mock, graphic: .icon(""), @@ -434,6 +444,8 @@ import Foundation clickAction: .mock ) + public static let mockNoClickAction = Self() + } extension Braze.InAppMessage.ClickAction { diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift index dc03081109..002ac684f9 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift @@ -7,7 +7,8 @@ import UIKit /// Assign an instance of this class to `braze.inAppMessagePresenter` to enable the presentation of /// in-app messages to the user. /// -/// To add gif support to the in-app message UI components, set a valid ``gifViewProvider``. +/// To add GIF support to the in-app message UI components, set a valid +/// ``gifViewProvider-swift.var``. @objc open class BrazeInAppMessageUI: NSObject, BrazeInAppMessagePresenter { diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift index cddb8f16e7..5e24e5f25b 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift @@ -154,7 +154,7 @@ extension BrazeInAppMessageUIDelegate { message: Braze.InAppMessage, view: InAppMessageView ) -> Bool { - return true + true } } diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift index b703a6967d..6ca10be173 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift @@ -33,7 +33,7 @@ extension BrazeInAppMessageUI { public var cornerRadius = 8.0 /// The content view shadow. - public var shadow: Shadow? = Shadow.default + public var shadow: Shadow? = Shadow.inAppMessage /// The minimum width (used when displayed as modal). public var minWidth = 320.0 diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift index bb1da01a13..b32248eb78 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift @@ -44,7 +44,7 @@ extension BrazeInAppMessageUI { public var cornerRadius = 8.0 /// The content view shadow. - public var shadow: Shadow? = Shadow.default + public var shadow: Shadow? = Shadow.inAppMessage /// The minimum width (used when displayed as modal). public var minWidth = 320.0 diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift index cdf2f844cf..bf3649feab 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift @@ -33,7 +33,7 @@ extension BrazeInAppMessageUI { public var cornerRadius = 8.0 /// The content view shadow. - public var shadow: Shadow? = Shadow.default + public var shadow: Shadow? = Shadow.inAppMessage /// The minimum width. public var minWidth = 320.0 @@ -81,10 +81,11 @@ extension BrazeInAppMessageUI { buttonsContainer?.layoutMargins = attributes.padding // Corner radius + shadowView.layer.cornerRadius = attributes.cornerRadius contentView.layer.cornerRadius = attributes.cornerRadius // Shadow - contentView.shadow = attributes.shadow + shadowView.shadow = attributes.shadow // Dimensions minWidthConstraint.constant = attributes.minWidth @@ -152,6 +153,8 @@ extension BrazeInAppMessageUI { return button }() + public lazy var shadowView = ShadowView(.inAppMessage) + public lazy var contentView: UIView = { let view = UIView() view.addSubview(imageContainerView) @@ -178,6 +181,7 @@ extension BrazeInAppMessageUI { super.init(frame: .zero) + addSubview(shadowView) addSubview(contentView) installInternalConstraints() @@ -227,7 +231,6 @@ extension BrazeInAppMessageUI { open override func layoutSubviews() { super.layoutSubviews() installPresentationConstraintsIfNeeded() - contentView.updateShadow() attributes.onLayout?(self) } @@ -276,6 +279,9 @@ extension BrazeInAppMessageUI { contentPositionConstraints = contentView.anchors.edges.lessThanOrEqual(layoutMarginsGuide) + contentView.anchors.center.align() + + // Shadow view + shadowView.anchors.edges.pin(to: contentView) } } diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift index 525cab225c..9ab60ec5c8 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift @@ -45,7 +45,7 @@ extension BrazeInAppMessageUI { public var cornerRadius = 8.0 /// The content view shadow. - public var shadow: Shadow? = Shadow.default + public var shadow: Shadow? = Shadow.inAppMessage /// The minimum width. public var minWidth = 320.0 @@ -121,9 +121,8 @@ extension BrazeInAppMessageUI { messageLabel.font = attributes.messageFont // Corner radius - contentView.layer.cornerRadius = attributes.cornerRadius - contentView.layer.masksToBounds = true shadowView.layer.cornerRadius = attributes.cornerRadius + contentView.layer.cornerRadius = attributes.cornerRadius // Shadow shadowView.shadow = attributes.shadow @@ -216,6 +215,8 @@ extension BrazeInAppMessageUI { return container }() + public lazy var shadowView = ShadowView(.inAppMessage) + public lazy var contentView: StackView = { let view = StackView( arrangedSubviews: [ @@ -244,8 +245,6 @@ extension BrazeInAppMessageUI { return button }() - public lazy var shadowView = UIView() - // MARK: - LifeCycle public init( @@ -312,7 +311,6 @@ extension BrazeInAppMessageUI { open override func layoutSubviews() { super.layoutSubviews() installPresentationConstraintsIfNeeded() - shadowView.updateShadow() attributes.onLayout?(self) } diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift index 1fc69d0314..e6e4b5a78e 100644 --- a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift @@ -45,7 +45,7 @@ extension BrazeInAppMessageUI { public var cornerRadius = 15.0 /// The content view shadow. - public var shadow: Shadow? = .default + public var shadow: Shadow? = .inAppMessage /// The minimum height. public var minHeight = 60.0 @@ -96,10 +96,11 @@ extension BrazeInAppMessageUI { messageLabel.font = attributes.font // Corner radius + shadowView.layer.cornerRadius = attributes.cornerRadius contentView.layer.cornerRadius = attributes.cornerRadius // Shadow - contentView.shadow = attributes.shadow + shadowView.shadow = attributes.shadow // Dimensions maxWidthConstraints.forEach { $0.constant = attributes.maxWidth } @@ -163,6 +164,8 @@ extension BrazeInAppMessageUI { return view }() + open lazy var shadowView = ShadowView(.inAppMessage) + open lazy var contentView: StackView = { let view = StackView( arrangedSubviews: [ @@ -193,6 +196,7 @@ extension BrazeInAppMessageUI { super.init(frame: .zero) + addSubview(shadowView) addSubview(contentView) installInternalConstraints() @@ -240,7 +244,6 @@ extension BrazeInAppMessageUI { open override func layoutSubviews() { super.layoutSubviews() installPresentationConstraintsIfNeeded() - contentView.updateShadow() attributes.onLayout?(self) } @@ -270,6 +273,9 @@ extension BrazeInAppMessageUI { // - position contentView.anchors.centerX.align() contentView.anchors.edges.lessThanOrEqual(layoutMarginsGuide) + + // Shadow view + shadowView.anchors.edges.pin(to: contentView) } } @@ -475,7 +481,7 @@ extension BrazeInAppMessageUI { @ViewBuilder static var variationPreviews: some View { - SlideupView(message: .mock) + SlideupView(message: .mockText) .preview(center: .required) .frame(maxHeight: 120) .previewDisplayName("Var. | Text") @@ -495,6 +501,11 @@ extension BrazeInAppMessageUI { .frame(maxHeight: 120) .previewDisplayName("Var. | Image") + SlideupView(message: .mockShort) + .preview(center: .required) + .frame(maxHeight: 120) + .previewDisplayName("Var. | Short") + SlideupView(message: .mockLong) .preview(center: .required) .frame(maxHeight: 120) @@ -567,26 +578,24 @@ extension BrazeInAppMessageUI { attributes.padding.right = 15 attributes.cornerRadius = 0 attributes.onPresent = { - let backgroundView = UIView() - $0.addSubview(backgroundView) + let backgroundView = ShadowView(.inAppMessage) + $0.insertSubview(backgroundView, at: 0) switch $0.message.slideFrom { case .top: - backgroundView.anchors.bottom.equal($0.anchors.top) + backgroundView.anchors.bottom.equal($0.anchors.bottom) case .bottom: - backgroundView.anchors.top.equal($0.anchors.bottom) + backgroundView.anchors.top.equal($0.anchors.top) + @unknown default: + backgroundView.anchors.top.equal($0.anchors.top) } backgroundView.anchors.edges.pin(axis: .horizontal) backgroundView.anchors.height.equal(1000) backgroundView.backgroundColor = $0.contentView.backgroundColor - $0.shadow = $0.contentView.shadow - $0.contentView.shadow = nil - } - attributes.onLayout = { - $0.updateShadow() + backgroundView.shadow = $0.shadowView.shadow + $0.shadowView.shadow = nil } attributes.onTheme = { - $0.backgroundColor = $0.contentView.backgroundColor - $0.subviews.last?.backgroundColor = $0.contentView.backgroundColor + $0.subviews.first?.backgroundColor = $0.contentView.backgroundColor } return attributes }() diff --git a/Sources/BrazeUI/PreviewProviders.swift b/Sources/BrazeUI/PreviewProviders.swift index 0ab24d2f6f..31afb8ab47 100644 --- a/Sources/BrazeUI/PreviewProviders.swift +++ b/Sources/BrazeUI/PreviewProviders.swift @@ -68,4 +68,31 @@ } + extension UIViewController { + + @available(iOS 13.0, *) + func preview() -> some View { + struct Wrapper: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let viewController: UIViewController + + init(viewController: UIViewController) { + self.viewController = viewController + } + + func makeUIViewController(context: Context) -> UIViewController { + viewController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + + } + } + return Wrapper(viewController: self) + .edgesIgnoringSafeArea(.all) + } + + } + #endif diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/Contents.json b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/Contents.json b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/Contents.json new file mode 100644 index 0000000000..ab7c630a38 --- /dev/null +++ b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "pin.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pin@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pin@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/pin.png b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/pin.png new file mode 100644 index 0000000000000000000000000000000000000000..2d15c6e1e81c79362ddef1fa77063d7bc905df4e GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAd3?%E9GuQzs#Q>iWS0IfaRN< zFU1-Ct#-b#_1LsIHJ&s67jb+(tA3`}eMWU<>&{mqY4c~!?dB9#^O<3gc&LSwztF&9 UGXIxv$3SlKboFyt=akR{0301T@Bjb+ literal 0 HcmV?d00001 diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/pin@2x.png b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/pin.imageset/pin@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..791fb204a4585bcde6aa1b8a2b1dc4cc5bee16dc GIT binary patch literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$FS^+*Gu0Wbdz$JXH1E`d@B*-tA zp{sRA}s~xg& zzW;xTPC&;5@pm7xQ&;|ZGBtAQ|K^Jly&$A>(O0-&@4Md(tG_bxFl=}kP zoY%RxW91q8v#;l+$1MD=U-#+Od5i1E1)udC|5?#kI3vMIYEH*YI0bg6Z7X69rmNO$^xPyt@1OkL0=Yu?J?}J7W0A^*`8Ri1j3p Zf7$++&DxuhtyK>SR!>(ymvv4FO#mrDk%s^P literal 0 HcmV?d00001 diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/Contents.json b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/Contents.json new file mode 100644 index 0000000000..ad6ae9d403 --- /dev/null +++ b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "retry.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "retry@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "retry@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/retry.png b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/retry.png new file mode 100644 index 0000000000000000000000000000000000000000..2d77b01acee77090c0eacbac94891aa5c1307e91 GIT binary patch literal 2078 zcmV+(2;ujMP)JW{ zn#rv1JDzn}cinyV+2`DI=h}kB+Iy{UeQTdH=j_M1)6`TgbE{qZJpY>`*I(VTD9AJWs zX>in@1?QcNI7EZX;mzfiY3wq6XLc_@&VWg7+8zT5wpy}a-50R-x!S~iW&?Ciw@&ow zzbV1LMf_5k)(U-7tuyXd;O|cWCAp<&MV!u0Dm?CvGg(6gK-&ZT8>)R8>ut9odJVKC z3DxWf90(sPCW$MjjC~a?4wW;3OXMpr#%!JBv%3@iT|K|P*7G1Xbj?OphBEve+6bfv=CVU z7F(zvzD3YcssgW&OZul!i|3@4Ssp@$CgRoSk)22qo`FyTNucHXo+cp}F5SXA#YZTh ziVcV-LGVjG7zr&wL+~@jsqF>@(0*TA2(R#gRBS8Sk`uYP-4ch(^1bwlCFV zat=}cvQvVz!!#hFE%*ruaqAumhvZDM!F6%)XB#N6|72W)gQ!{f?#tEO7r@3lD)<#? zcmh|Yfe?QH`d$F;9AKn4w=b!}g+LNGyXBSfNE9Ag_A?3R15NND`a@C}7MdL9cckQ` zhKB>u3)O>3YgZhaTon>GtuC5ci;u!_;wT`*Mr4X zB+a+J*6O<*h2MnlBxtiDAB*-fsB1tcC|ppJ^RgxC2+G`d%YQWaw+7}$FkSflEQtzR zLR<56s90i?I}}M;oQMhw*gNRmz22n?za79oIsh|Kaq?|R6uDyBG~qli_%!Up3R`l4 zA5;LIS<>WN078qCOR%(6;tf`zo0_`elN?95%EhHP5ifzqs?yw!4}4^f6wV7$ql2Le^~NxrBE{3h!_MT)szD5|cY!Adhb-V*OAlOq)q=1X?Gv zeOU^_QHbl?%$%Q6auUOwaO?o2&qar4IQ*iI^SD^e;xdv{1{MO5h;j4K*bO?067ar&DhH}-k7gSXLD{x;5)yGA2 z8zZNoEcZWIFOG+^@1Y+-yP-T{?uBOM;pfc5p@SyeO)Q6|4V3%rc~I|35pkciBFM8g z|Kh%SeTdifoo_*bH#~#*4zWw1^%UO7d!hPRZ*R(VMGv48gtmzEpGK?^lTsV%@|BTs zXv@Ww@tR~9vbRDtv9wp##9Ul5cc7aS#`sj;GGma)Q=(j~*e&oKmX|cnYlVFsx(B)) zdJnW#IST}O`z+&qEi3WyMG++w{u+K6oLGJ(t5vwK;N&NqVtI&Z4kdRDZdGV<0FFb0clAFw_*aA^bE6^y5=*k+I~P!d>$`A^y*3~(6f)hx&pQ0%5&hhJ1ng|+ zadgDO4Z{u6QFa^JTw+AHDd9T)r8qKL?gFh;eNnQ241L~QZjbqEh8CU`&!(dXDtA4P z)z}V-UQ-s+0L~6~< zOU}or9Xd5HfyyJ-YN!lK>>`E9+kC{qOa3$R5?nCYAO^_(wq-CBsqpEvSq~TaJzKLgWsxXmZ`|M(oH!s)Uj772E;UV~}<~{IkMn zvZ1)vuY^h=m*sya@T+c-(gLzhXwp|Dj$;725IQJwE~@lFT{l4cxM$#I<`(EFQ2qe! z5>ABNHcx{tfv$sc3$D3b?M=W;fHq9#pmHzNHMn~C)}q#ohFz_vFdxymP!6z&3+L1I zT+EsD)F2!#e8d_d$Dk-DMn2xu(a^u3+=}v)NaaMvC8}=n9{@FqKV}?lJOBUy07*qo IM6N<$f{g^tn*aa+ literal 0 HcmV?d00001 diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/retry@2x.png b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/retry@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a9fe0c6bdfec234aef36b4b058b2798cb124edcd GIT binary patch literal 4584 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 literal 0 HcmV?d00001 diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/retry@3x.png b/Sources/BrazeUI/Resources/Assets.xcassets/ContentCard/retry.imageset/retry@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3f78e96e81ff756957c965473988f94a8e19aa77 GIT binary patch literal 7406 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 literal 0 HcmV?d00001 diff --git a/Sources/BrazeUI/Resources/Localization/Base.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/Base.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..0aecfcf21f --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/Base.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "We have no updates.\nPlease check again later."; diff --git a/Sources/BrazeUI/Resources/Localization/ar.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/ar.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..7135908410 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/ar.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "ليس لدينا أي تحديث\n يرجى التحقق مرة أخرى لاحقاً"; diff --git a/Sources/BrazeUI/Resources/Localization/cs.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/cs.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..723111c08d --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/cs.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Nemáme žádné aktualizace.\nZkontrolujte prosím znovu později."; diff --git a/Sources/BrazeUI/Resources/Localization/da.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/da.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..f655cddaa2 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/da.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Vi har ingen updates.\nPrøv venligst senere"; diff --git a/Sources/BrazeUI/Resources/Localization/de.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/de.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..d2314f21ce --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/de.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Derzeit sind keine Updates verfügbar.\nBitte später noch einmal versuchen."; diff --git a/Sources/BrazeUI/Resources/Localization/en.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/en.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..0aecfcf21f --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/en.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "We have no updates.\nPlease check again later."; diff --git a/Sources/BrazeUI/Resources/Localization/es-419.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/es-419.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..a85f5d0cb6 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/es-419.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "No tenemos ninguna actualización.\nVuelva a verificar más tarde."; diff --git a/Sources/BrazeUI/Resources/Localization/es-MX.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/es-MX.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..a85f5d0cb6 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/es-MX.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "No tenemos ninguna actualización.\nVuelva a verificar más tarde."; diff --git a/Sources/BrazeUI/Resources/Localization/es.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/es.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..08970a0c92 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/es.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "No tenemos actualizaciones.\nPor favor compruébelo más tarde."; diff --git a/Sources/BrazeUI/Resources/Localization/et.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/et.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..1dffe8f0ed --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/et.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Uuendusi pole praegu saadaval.\nProovige hiljem uuesti."; diff --git a/Sources/BrazeUI/Resources/Localization/fi.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/fi.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..88b5b9db08 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/fi.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Päivityksiä ei ole saatavilla.\nTarkista myöhemmin uudelleen."; diff --git a/Sources/BrazeUI/Resources/Localization/fil.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/fil.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..5f2a0bec80 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/fil.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Wala kaming mga update.\nMangyaring suriin muli sa ibang pagkakataon."; diff --git a/Sources/BrazeUI/Resources/Localization/fr.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/fr.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..16a73e05b8 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/fr.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Aucune mise à jour disponible.\nVeuillez vérifier ultérieurement."; diff --git a/Sources/BrazeUI/Resources/Localization/he.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/he.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..4b60997514 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/he.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "אין לנו עדכונים\nבבקשה בדוק שוב בקרוב"; diff --git a/Sources/BrazeUI/Resources/Localization/hi.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/hi.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..c11b6eadad --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/hi.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "हमारे पास कोई अपडेट नहीं हैं। कृपया बाद में फिर से जाँच करें.।"; diff --git a/Sources/BrazeUI/Resources/Localization/id.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/id.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..07f3a5d4ca --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/id.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Kami tidak memiliki pembaruan.\nCoba lagi nanti."; diff --git a/Sources/BrazeUI/Resources/Localization/it.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/it.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..dc3ad14cab --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/it.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Non ci sono aggiornamenti.\nRicontrollare più tardi."; diff --git a/Sources/BrazeUI/Resources/Localization/ja.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/ja.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..a93c64378b --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/ja.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "アップデートはありません。\n後でもう一度確認してください。"; diff --git a/Sources/BrazeUI/Resources/Localization/km.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/km.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..26dec289fe --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/km.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "យើងមិនមានការធ្វើបច្ចុប្បន្នភាពទេ។ សូមពិនិត្យមើលម្តងទៀតនៅពេលក្រោយ."; diff --git a/Sources/BrazeUI/Resources/Localization/ko.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/ko.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..291db0c4f2 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/ko.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "업데이트가 없습니다.\n다음에 다시 확인해 주십시오."; diff --git a/Sources/BrazeUI/Resources/Localization/lo.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/lo.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..05731c3a6e --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/lo.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "ພວກ​ເຮົາ​ບໍ່​ມີ​ການ​ອັບ​ເດດ.\nກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ."; diff --git a/Sources/BrazeUI/Resources/Localization/ms.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/ms.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..bdee085d32 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/ms.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Tiada kemas kini.\nSila periksa kemudian."; diff --git a/Sources/BrazeUI/Resources/Localization/my.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/my.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..1d41a245d5 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/my.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "ကၽႊႏု္ပ္ တို႕တြင္ အသစ္တင္ျပရန္မရွိပါ။ ေက်းဇူးျပဳ၍ ေနာင္တြင္ ထပ္စစ္ပါ။ ."; diff --git a/Sources/BrazeUI/Resources/Localization/nb.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/nb.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..6679da4b50 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/nb.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Vi har ingen oppdateringer.\nVennligst sjekk igjen senere."; diff --git a/Sources/BrazeUI/Resources/Localization/nl.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/nl.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..ff22828905 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/nl.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Er zijn geen updates.\nProbeer het later opnieuw."; diff --git a/Sources/BrazeUI/Resources/Localization/pl.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/pl.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..8d92fe18e4 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/pl.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Brak aktualizacji.\nProszę sprawdzić ponownie później."; diff --git a/Sources/BrazeUI/Resources/Localization/pt-PT.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/pt-PT.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..524033d319 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/pt-PT.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Não temos atualizações.\nPor favor, verifique mais tarde."; diff --git a/Sources/BrazeUI/Resources/Localization/pt.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/pt.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..c464c569f1 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/pt.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Não temos nenhuma atualização.\nVerifique novamente mais tarde."; diff --git a/Sources/BrazeUI/Resources/Localization/ru.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/ru.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..6eaec14b70 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/ru.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Обновления недоступны.\nПожалуйста, проверьте снова позже."; diff --git a/Sources/BrazeUI/Resources/Localization/sv.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/sv.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..a981335fca --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/sv.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Det finns inga uppdateringar.\nFörsök igen senare."; diff --git a/Sources/BrazeUI/Resources/Localization/th.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/th.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..43690beac0 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/th.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "เราไม่มีการอัพเดต กรุณาตรวจสอบภายหลัง."; diff --git a/Sources/BrazeUI/Resources/Localization/uk.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/uk.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..78f46d27bf --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/uk.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Оновлення недоступні.\nБудь ласка, перевірте знову пізніше."; diff --git a/Sources/BrazeUI/Resources/Localization/vi.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/vi.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..1286957fdd --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/vi.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "Chúng tôi không có cập nhật nào.\nVui lòng kiểm tra lại sau."; diff --git a/Sources/BrazeUI/Resources/Localization/zh-HK.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/zh-HK.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..df982dfcb6 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/zh-HK.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "暫時沒有更新.\n請稍候再試."; diff --git a/Sources/BrazeUI/Resources/Localization/zh-Hans.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/zh-Hans.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..45648a95b4 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/zh-Hans.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "暂时没有更新.\n请稍后再试."; diff --git a/Sources/BrazeUI/Resources/Localization/zh-Hant.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/zh-Hant.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..df982dfcb6 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/zh-Hant.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "暫時沒有更新.\n請稍候再試."; diff --git a/Sources/BrazeUI/Resources/Localization/zh-TW.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/zh-TW.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..df982dfcb6 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/zh-TW.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "暫時沒有更新.\n請稍候再試."; diff --git a/Sources/BrazeUI/Resources/Localization/zh.lproj/ContentCardsLocalizable.strings b/Sources/BrazeUI/Resources/Localization/zh.lproj/ContentCardsLocalizable.strings new file mode 100644 index 0000000000..45648a95b4 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/zh.lproj/ContentCardsLocalizable.strings @@ -0,0 +1 @@ +"braze.content-cards.no-card.text" = "暂时没有更新.\n请稍后再试.";