SwiftUI Bridge (SFWidget)
Intro
Please make sure to read iOS Bridge and Getting Started pages before moving forward.
SwiftUI Component
SwiftUI component is available since SDK version 5.3.2
SwiftUI component allows you to implement the Outbrain widget without any additional code which handles the widget height and paid recommendation clicks internally. This component is a SwiftUI view which can be directly inserted into the SwiftUI view hierarchy.
OutbrainWidgetView(
url: "https://example.com",
widgetId: "MB_1",
widgetIndex: 0,
installationKey: "installation_key",
darkMode: false) { url in
// Handle click on oranic recommendation
}
By default OutbrainWidgetView
doesn't send any widget events, but in case you wish to receive the events you can change your implementation to the following:
OutbrainWidgetView(
url: "https://example.com",
widgetId: "MB_1",
widgetIndex: 0,
installationKey: "installation_key",
darkMode: false,
onOrganicRecClick: { url in
// Handle click on oranic recommendation
}) { eventName, params in
// Handle widget event
}
In order to avoid widget re-creation it is recommended to create it in the @ObservableObject
. Please consider the following example:
final class OBWidgetViewModel: ObservableObject {
var widgetView: OutbrainWidgetView? = nil
init(
url: String,
widgetId: String,
widgetIndex: Int,
installationKey: String,
userId: String? = nil,
darkMode: Bool = false,
extId: String? = nil,
extSecondaryId: String? = nil,
OBPubImp: String? = nil,
onOrganicRecClick: ((URL) -> Void)? = nil,
onWidgetEvent: ((String, [String: Any]) -> Void)? = nil
) {
widgetView = OutbrainWidgetView(
url: url,
widgetId: widgetId,
widgetIndex: widgetIndex,
installationKey: installationKey,
userId: userId,
darkMode: darkMode,
extId: extId,
extSecondaryId: extSecondaryId,
OBPubImp: OBPubImp,
onOrganicRecClick: onOrganicRecClick,
onWidgetEvent: onWidgetEvent
)
}
}
In your SwiftUI view you should create and use the view model like this:
struct SwiftUIView: View {
@StateObject private var obViewModel: OBWidgetViewModel
init(
onOrganicClick: ((URL) -> Void)?
) {
self._obViewModel = .init(wrappedValue: .init(
url: "https://mobile-demo.outbrain.com",
widgetId: "MB_1",
widgetIndex: 0,
installationKey: "NANOWDGT01",
darkMode: false,
onOrganicRecClick: {
onOrganicClick?($0)
}, onWidgetEvent: { eventName, additionalData in
print(eventName)
}
))
}
var body: some View {
ScrollView {
LazyVStack {
ArticleBody()
ArticleBody()
ArticleBody()
}
obViewModel.widgetView
}
}
}
UIViewRepresentable approach
In case the SwiftUI component didn't work as expected there is a possibility to use the SFWidget
with the UIViewRepresentable
approach. Please follow the instructions below.
Copy/Paste SFWidgetObservable.swift
Please copy/paste the following content to a file named SFWidgetObservable.swift
and add it to your app module in Xcode. This file contains the following:
-
SFWidgetObservable
– which is anObservableObject
which is shared between the SDK and the app code (to receive the URL of the recommendation being clicked for example. -
SFWidgetWrapper
– which is anUIViewRepresentable
which basically wrapsSFWidget
and contains the mandatory parameters needed. -
OBSafariView
– which is anUIViewRepresentable
which basically wrapsSFSafariViewController
with the URL to open.
import Foundation
import SafariServices
import SwiftUI
import OutbrainSDK
// Our observable object class
class SFWidgetObservable: ObservableObject {
@Published var showSafari: Bool = false
@Published var url: URL?
@Published var widgetHeight: CGFloat = 800.0
}
struct SFWidgetWrapper: UIViewRepresentable {
@EnvironmentObject var sfWidgetObservable: SFWidgetObservable
let widgetId:String
let baseURL:String
let installationKey:String
let myCustomDelegate = CustomSFWidgetDelegate()
func updateUIView(_ uiView: SFWidget, context: Context) {
uiView.configure(
with: myCustomDelegate,
url: baseURL,
widgetId: widgetId,
widgetIndex: 0,
installationKey: installationKey,
userId: nil,
darkMode: false,
isSwiftUI: true
)
myCustomDelegate.sfWidgetObservable = sfWidgetObservable
}
func makeUIView(context: Context) -> SFWidget {
SFWidget()
}
}
struct OBSafariView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: UIViewControllerRepresentableContext<OBSafariView>) -> SFSafariViewController {
return SFSafariViewController(url: url)
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<OBSafariView>) {
// No-op
}
}
Copy/Paste CustomSFWidgetDelegate.swift
Please copy\paste the following content to a file named CustomSFWidgetDelegate.swift
and add it to your app module in Xcode.
This file contains a simple working example of implementing the necessary SFWidgetDelegate
(which is responsible for handling clicks for example)
import Foundation
import OutbrainSDK
// Our CustomSFWidgetDelegate implementation class that expects to find a SFWidgetObservable object
// in the environment, and set if needed.
class CustomSFWidgetDelegate : NSObject {
var sfWidgetObservable: SFWidgetObservable?
}
extension CustomSFWidgetDelegate : SFWidgetDelegate {
func onRecClick(_ url: URL) {
guard let sfWidgetObservable else { return }
sfWidgetObservable.url = url
sfWidgetObservable.showSafari = true
}
func didChangeHeight(_ newHeight: CGFloat) {
print("didChangeHeight \(newHeight)")
sfWidgetObservable?.widgetHeight = newHeight
}
}
Integrate SFWidgetWrapper in your app ContentView
This is the SwiftUI file where the app developer wants to include Outbrain Bridge (SFWidget) in the views hirarechy.
-
Please note where we place
SFWidgetWrapper
- this View will contain the entire Outbrain feed. -
init() method should call
initializeOutbrain()
with the partner key (given by Outbrain Account Manager). -
SFWidgetWrapper
- make sure to pass it the “article URL”, “widget id” and “installationKey” (partner key). -
SFWidgetWrapper
- make sure to set the frame height according to sfWidgetObservable.widgetHeight which dynamically changes as content is loaded into the feed. -
.fullScreenCover
with OBSafariView to allow SafariViewController to display the recommendation URL on click event. -
.environmentObject(sfWidgetObservable)
- to configure the environmentObject which will be shared between the SDK and the app.
See complete example below:
import SwiftUI
import OutbrainSDK
struct ContentView: View {
@StateObject var sfWidgetObservable = SFWidgetObservable()
let widgetId = "MB_1"
let baseURL = "https://mobile-demo.outbrain.com"
let installationKey = "NANOWDGT01"
init() {
Outbrain.initializeOutbrain(withPartnerKey: "iOSSampleApp2023")
}
var body: some View {
ScrollView(.vertical) {
VStack(spacing:0) {
Image("article_image", bundle: Bundle.main)
.resizable()
.aspectRatio(16/9, contentMode: .fill)
Text("The Guardian")
.padding()
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 80.0)
.background(.blue)
.foregroundColor(.white)
Text("Suarez: Messi Was Born Great, Ronaldo Made Himself Great")
.font(.system(size: 24))
.fontWeight(.medium)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets(top: 20, leading: 5, bottom: 20, trailing: 0 ))
ArticleBody()
ArticleBody()
ArticleBody()
SFWidgetWrapper(widgetId: widgetId, baseURL: baseURL, installationKey: installationKey)
.frame(height: sfWidgetObservable.widgetHeight)
.padding(EdgeInsets(top: 20, leading: 10, bottom: 20, trailing: 10))
}
}
.fullScreenCover(isPresented: $sfWidgetObservable.showSafari) {
OBSafariView(url: sfWidgetObservable.url!)
}
.environmentObject(sfWidgetObservable)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ArticleBody: View {
let loremIpsem = "Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda."
var body: some View {
Text(loremIpsem)
.font(.body)
.frame(maxWidth: .infinity,alignment: .leading)
.padding(.horizontal, 5)
.padding(.top, 20)
}
}