Skip to main content

SwiftUI Bridge (SFWidget)

Intro

info

Please make sure to read iOS Bridge and Getting Started pages before moving forward.

SwiftUI Component

info

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:

  1. SFWidgetObservable – which is an ObservableObject which is shared between the SDK and the app code (to receive the URL of the recommendation being clicked for example.

  2. SFWidgetWrapper – which is an UIViewRepresentable which basically wraps SFWidget and contains the mandatory parameters needed.

  3. OBSafariView – which is an UIViewRepresentable which basically wraps SFSafariViewController 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.

  1. Please note where we place SFWidgetWrapper - this View will contain the entire Outbrain feed.

  2. init() method should call initializeOutbrain() with the partner key (given by Outbrain Account Manager).

  3. SFWidgetWrapper - make sure to pass it the “article URL”, “widget id” and “installationKey” (partner key).

  4. SFWidgetWrapper - make sure to set the frame height according to sfWidgetObservable.widgetHeight which dynamically changes as content is loaded into the feed.

  5. .fullScreenCover with OBSafariView to allow SafariViewController to display the recommendation URL on click event.

  6. .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)
}
}