iOS Swift UI Integration
Pre-Requirements
Download and install XCODE (opens in a new tab). Only available in MacOS systems.
The source code of the next step-by-step guide can be downloaded here (opens in a new tab)
Steps
- Create new project using SwiftUI.
- Create HTMLTemplate file.
- Create WebViewCoordinator class.
- Create WebViewWrapper structure.
- Create VideoPlayerConfiguration structure.
- Create FullscreenContentView.
- Instantiate the WebViewCoordinator.
- Add WebviewWrapper in the main view.
- Create Subscription and EventHandlers for:
- Interpolate HTMLTemplate with configuration object.
- Load processed HTML in the Webview.
- Prevent device from locking when user isn't interacting.
- Fullscreen Landscape Support with Notch Awareness.(Optional)
- Create Custom Betslip.
- Handle closing the player
Steps detailed
1. Create new project using SwiftUI.
Open XCode and create a new project.
File > New > Project or Shift + Command + N
Make sure that the interface selected is SwiftUI.
2. Create HTMLTemplate file.
Create a file named HTMLTemplate.swift
Feel free to create this file wherever you find it convenient.
Create getHTMLString function
This function will interpolate dynamic values based on the configuration object. After this, it will return a string with the HTML that you want to render inside the WebView. You can take a look at our template definition here (opens in a new tab), and in order to get better understanding of this template feel free to read the breakdown section here.
3. WebViewCoordinator class.
Create WebViewCoordinator class file. Feel free to create this file wherever you find it convenient.
This class is responsible of managing the WebView instance and disabling the AVPlayer. We've done this because we want to use the player's custom control bar and functionality.
These are the functionalities that this class provides:
-
Ensuring the proper functioning of the video player when transitioning between non-fullscreen and fullscreen modes.
-
Handling the communication between the HTML content and the app. This communication is done with the userContentController function.
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { // Handle the message received from JavaScript if let data = message.body as? [String : Any], let type = data["type"] { messageHandler?("\(type)", nil) if let payload = data["payload"] { messageHandler?("\(type)", payload) } } }
Your file should look like this:
import WebKit
class WebViewCoordinator: NSObject, ObservableObject, WKScriptMessageHandler, WKNavigationDelegate {
@Published var webView: WKWebView
var messageHandler: ((String, Any?) -> Void)?
override init() {
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.allowsPictureInPictureMediaPlayback = false
let userContentController = WKUserContentController()
configuration.userContentController = userContentController
webView = WKWebView(frame: .zero, configuration: configuration)
super.init()
userContentController.add(self, name: "gsVideoPlayerBridge")
webView.navigationDelegate = self
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// Handle the message received from JavaScript
if let data = message.body as? [String : Any],
let type = data["type"] {
messageHandler?("(type)", nil)
if let payload = data["payload"] {
messageHandler?("(type)", payload)
}
}
}
}
4. Create WebViewWrapper structure.
Create WebViewWrapper class file. Place this file wherever you find it convenient.
This class is responsible for wrapping the WebView and configuring its default values.
- The WebView has to be wrapped so that it can be used in the ContentView.
- The WebView's scrollView is configured so that it does not bounce nor scrolls.
webView.scrollView.bounces = false webView.scrollView.isScrollEnabled = false
Your code should look like this:
import SwiftUI
import WebKit
struct WebViewWrapper: UIViewRepresentable {
let webView: WKWebView
func makeUIView(context: Context) -> WKWebView {
webView.scrollView.bounces = false
webView.scrollView.isScrollEnabled = false
webView.backgroundColor = .black
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
// No-op
}
}
5. Create VideoPlayerConfiguration structure.
Create VideoPlayerConfiguration structure file. Place this file wherever you find it convenient.
This structure contains all parameters needed in the HTMLTemplate interpolation.
struct VideoPlayerConfiguration {
var customerId: String = ""
var fixtureId: String = ""
var playerWidth: String = "100vw"
var playerHeight: String = "100vh"
var controlsEnabled: String = "true"
var audioEnabled: String = "true"
var allowFullScreen: String = "true"
var bufferLength: String = "2"
var autoplayEnabled: String = "true"
}
6. Create FullScreenContentView.
Create the FullScreenContentView file.
This file is in charge of using the WebViewCoordinator, injecting the HTML into the webview and handling fullscreen and device rotation controls.
a. Instantiate the WebViewCoordinator.
You should instantiate it as a private @StateObject
struct FullscreenContentView: View {
@StateObject private var webViewCoordinator = WebViewCoordinator()
...
}
b. Add WebViewWrapper in the main view.
Add WebViewWrapper to the body.
var body: some View {
GeometryReader { metrics in
VStack(spacing: 0) {
GeometryReader { geometry in
ZStack {
WebViewWrapper(webView: webViewCoordinator.webView)
.frame(width: metrics.size.width, height: 300)
}
}
}
c. Create Subscription and EventHandlers.
-
Device Rotation:
You should create a function that tells the app to change orientation. This will be used when changing between fullscreen modes and when loading the views.
func changeOrientation(to orientation: UIInterfaceOrientationMask) { // tell the app to change the orientation let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation)) windowScene?.keyWindow?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations() }
-
Fullscreen state:
- You should implement a function that Toggles the HTML video element's fullscreen as true, this should happen when the app is portrait and is presented.
func onToggleFullscreen() { // This is needed to avoid unkown orientation value let isPortrait = UIDevice.current.orientation.rawValue == 0 ? UIScreen.main.bounds.size.width < UIScreen.main.bounds.size.height : UIDevice.current.orientation.rawValue == 1 if (!isPortrait && !isPresented) || (isPortrait && isPresented) { isPresented.toggle() let script = """ document.querySelector(".video-container")?.classList.toggle("full-screen") """ webViewCoordinator.webView.evaluateJavaScript(script) } }
-
Subscription and EventHandlers integration:
-
Create Message Handler
Your "messageHandler" function should receive a type and use it to trigger the fullscreen state or the orientation change event.
func messageHandler(type: String) { if (type == "toggleFullscreen") { changeOrientation(to: UIDevice.current.orientation.isLandscape ? .portrait : .landscape) isPresented.toggle() } else if (type == "init") { isVideoPlayerReady = true onToggleFullscreen() } }
-
Add the "messageHandler" to the view's body, also add events for
onAppear
,onReceive
,fullScreenCover
var body: some View { GeometryReader { metrics in VStack(spacing: 0) { GeometryReader { geometry in ZStack { WebViewWrapper(webView: webViewCoordinator.webView) .frame(width: metrics.size.width, height: 300) } } } .onAppear { // Load initial web content webViewCoordinator.messageHandler = messageHandler updateVideoURL() } .onDisappear() { // Re-enable idle timer when the view disappears UIApplication.shared.isIdleTimerDisabled = false } .onReceive(rotationChangePublisher) { _ in if isVideoPlayerReady { onToggleFullscreen() } } } }
-
d. Interpolate HTMLTemplate with configuration object.
Call the getHTMLTemplate from the HTMLTemplate.swift function with an instance of the VideoPlayerConfiguration.
let configuration = VideoPlayerConfiguration()
let htmlString = getHTMLString(configuration: configuration)
e. Load processed HTML in the Webview.
You need a baseUrl, which can be any URL that is used to load the WebView.
In our case, we use the one seen on the code "https://www.example.com"
let baseURL = "https://www.example.com"
webViewCoordinator.webView.loadHTMLString(
htmlString,
baseURL: URL(string: String(format: baseURL)))
f. Prevent device from locking when user isn't interacting :
In the video view it is recommended to avoid the automatic screen lock, so that the user enjoys a fluid experience without having to constantly touch the screen. In order to enable this option you must add the following function:
init() {
UIApplication.shared.isIdleTimerDisabled = true
}
Also to keeps this feature alive, even when user mute or unmute the video player, we need to refresh the state of isIdleTimerDisabled behavior, to get this you need to declare this two vars:
@State private var isIdleTimerDisabled = false
let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect()
}
Add the handler to set the isIdleTimerDisabled on the VStack component:
onReceive(timer) { _ in
isIdleTimerDisabled.toggle()
UIApplication.shared.isIdleTimerDisabled = isIdleTimerDisabled
}
g. Fullscreen Landscape Support with Notch Awareness [Optional] :
This step is optional. but if you want to respect the safe area which can reduce the video size, you have to follow this code version: View FullscreenContentView.swift on GitHub (opens in a new tab)
We want to offer the best fullscreen experience of the embedded video player when rotating the device to landscape mode. To ensure that the content should never hidden behind the device's notch (also known as the sensor housing or camera cutout) and that all interactive UI remains fully visible.
Key points:
-
Dynamic Notch Detection: The app now detects which side the notch is on (left or right) when the device is rotated into landscape.
-
Safe Area Handling: The video player automatically respects the safe area insets to prevent UI elements from being obstructed.
-
Consistent Fullscreen Behavior: Even when the orientation is changed programmatically or manually by the user, the layout adjusts in real time.
Device Compatibility:
-
iPhones with notch (X and newer): notch padding applied dynamically
-
iPhones without notch: no layout changes
-
iPads: unaffected, layout remains consistent
Why This Matters:
Without proper notch handling, key content (like buttons, stats, or the video stream) may be partially hidden or clipped on devices with a notch. This functionality ensures:
-
Improved usability
-
Better UX consistency
-
Correct video aspect ratio
Adding notch detection:
Let's create to types one for orientation source and other one to identify the position of the notch:
enum NotchPosition: String {
case left = "left"
case right = "right"
case top = "top"
case none = "No detected notch"
}
enum OrientationSource: String {
case programatically = "programatically"
case accelerometer = "accelerometer"
case none = "none"
}
We need the following two funtions to get the safe areas and the notch detection logic:
private func getSafeAreaInsets() -> UIEdgeInsets {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first else {
return .zero
}
return window.safeAreaInsets
}
private func detectNotchSide(safeArea: UIEdgeInsets) -> NotchPosition {
let orientation = UIDevice.current.orientation
// Landscape left or programmatic - check left edge
if ((orientation == .landscapeLeft && safeArea.left > 0) || orientationSource == .programatically) {
return .left
}
// Landscape right - check right edge
if orientation == .landscapeRight && safeArea.right > 0 {
return .right
}
// Face up/down - use previous notch position
if orientation == .faceUp || orientation == .faceDown || orientation == .portraitUpsideDown {
return notchUpdateChange
}
// Portrait - check top edge
if orientation == .portrait && safeArea.top > 0 {
return .top
}
// No notch detected
return notchUpdateChange
}
Now we are going to add a state for the orientation source
@State private var orientationSource = OrientationSource.none
@State private var notchUpdateChange = NotchPosition.none
The orientationSource state should be assigned based in two situations:
- when the user turns the phone from portrait to landscape. You need to assign the orientation as accelerometer in the onToggleFullscreen function
func onToggleFullscreen() {
// This is needed to avoid unkown orientation value
let isPortrait = UIDevice.current.orientation.rawValue == 0 || UIDevice.current.orientation.isFlat
? UIScreen.main.bounds.size.width < UIScreen.main.bounds.size.height
: UIDevice.current.orientation == .portrait || UIDevice.current.orientation.isFlat
orientationSource = .accelerometer
...
- when the user taps the fullscreen button. You need to assign the orientation as programatically in the changeOrientation function
func changeOrientation(to orientation: UIInterfaceOrientationMask) {
// tell the app to change the orientation
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation))
windowScene?.keyWindow?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
DispatchQueue.main.async {
notchUpdateChange = detectNotchSide(safeArea: getSafeAreaInsets())
}
orientationSource = .programatically
}
Now, let's add a in the onReceive to execute detectNotchSide side when a rotation occurs
.onReceive(rotationChangePublisher) { _ in
onToggleFullscreen()
DispatchQueue.main.async {
notchUpdateChange = detectNotchSide(safeArea: getSafeAreaInsets())
}
}
...
Finally you need to add the proper padding and ignore all safeAreaInsets in the ZStack
...
.edgesIgnoringSafeArea(.all)
.padding(.leading, notchUpdate == .left ? 1 : 0)
.padding(.trailing, notchUpdate == .right ? 1 : 0)
.padding(.top, notchUpdateChange == .top ? 1 : 0)
...
Your code should look like this:
import SwiftUI
import WebKit
struct FullscreenContentView: View {
@StateObject private var webViewCoordinator = WebViewCoordinator()
@State private var url: String = ""
@State private var isPresented = false
@State private var showBetslip = false
@State private var showToast = false
@State private var fakeFixtureId: String = "10462057"
@State private var immersiveFixtureId: String = ""
@State private var environment: String = "PROD"
@State private var culture: String = ""
@State private var source: DeliveryStream = DeliveryStream(id:"",name:"DEFAULT")
@State private var sources: Array<DeliveryStream> = []
@State private var apiKey: String = ""
@State private var customerId: String = ""
@State private var showPlayerTracking: Bool = false
@State private var triggerOnboarding: String = "Disabled"
@State private var customer: VideoCustomer = VideoCustomersKt.videoCustomers[0]
@StateObject private var betslipData = BetslipData()
@StateObject private var betslipCoordinates = BetslipCoordinates()
@State private var userEmail: String = ""
@State private var deviceID: String = DeviceIDManager().deviceID
@State private var isFullScreen = false
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State private var orientationSource = OrientationSource.none
@State private var notchUpdateChange = NotchPosition.none
private let rotationChangePublisher = NotificationCenter.default
.publisher(for: UIDevice.orientationDidChangeNotification)
@State private var isIdleTimerDisabled = false
let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect()
@State private var selectedTab: Int = 0
let tabs: [Tab] = [
.init(title: "Video"),
.init(title: "Stats")
]
var body: some View {
GeometryReader { metrics in
ScrollView {
VStack(spacing: 0) {
HeaderPlaceholder(geometry: metrics)
GeometryReader { geometry in
ZStack {
VStack(spacing: 0) {
if(!isPresented){
Tabs(tabs: tabs, geoWidth: geometry.size.width, selectedTab: $selectedTab).background(Color.background)
}
TabView (selection: $selectedTab,
content: {
WebViewWrapper(webView: webViewCoordinator.webView)
.tag(0)
.gesture(isPresented ? DragGesture() : nil)
Text("Stats")
.tag(1)
})
.background(.black)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
if showBetslip {
CustomBetslip(betslipData: betslipData, showToast: $showToast, showBetslip: $showBetslip, betslipCoordinates: betslipCoordinates)
}
if showToast {
CustomToast(showToast: $showToast)
}
}
.frame(width: metrics.size.width, height: geometry.size.height)
.offset(x: 0, y: isPresented ? -geometry.frame(in: .global).minY : 0)
}
.frame(height: isPresented ? metrics.size.height : 300)
LoadVideoForm(
fakeFixtureId: $fakeFixtureId,
immersiveFixtureId: $immersiveFixtureId,
culture: $culture,
environment: $environment,
source: $source,
sources: $sources,
apiKey: $apiKey,
customerId: $customerId,
showPlayerTracking: $showPlayerTracking,
triggerOnboarding: $triggerOnboarding,
customer: $customer,
userEmail: $userEmail,
action: updateVideoURL
)
BodyPlaceholder()
}
.frame(height: metrics.size.height, alignment: .top)
.background(Color.background)
.onAppear {
// Load initial web content
webViewCoordinator.messageHandler = messageHandler
updateVideoURL(reset: true)
}
.onDisappear {
webViewCoordinator.onDisappear()
}
.onReceive(rotationChangePublisher) { _ in
onToggleFullscreen()
DispatchQueue.main.async {
notchUpdateChange = detectNotchSide(safeArea: getSafeAreaInsets())
}
}.onReceive(timer) { _ in
// Toggle the idle timer state
isIdleTimerDisabled.toggle()
UIApplication.shared.isIdleTimerDisabled = isIdleTimerDisabled
}
}
.ignoresSafeArea(.all)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
guard let userInfo = notification.userInfo,
let keyboardSize = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
return
}
withAnimation {
scrollViewBottomInset = keyboardSize.height
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { notification in
withAnimation {
scrollViewBottomInset = 0
}
}
.padding(.bottom, keyboardHeight())
}
.onAppear {
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
notchUpdateChange = detectNotchSide(safeArea: getSafeAreaInsets())
}
.edgesIgnoringSafeArea(.bottom)
.ignoresSafeArea(.all)
.padding(.leading, notchUpdateChange == .left ? 1 : 0)
.padding(.trailing, notchUpdateChange == .right ? 1 : 0)
.padding(.top, notchUpdateChange == .top ? 1 : 0)
.preferredColorScheme(.dark)
}
@State private var scrollViewBottomInset: CGFloat = 0
private func keyboardHeight() -> CGFloat {
return scrollViewBottomInset
}
init() {
UIApplication.shared.isIdleTimerDisabled = true
}
func onDismiss() {
changeOrientation(to: .portrait)
}
private func getSafeAreaInsets() -> UIEdgeInsets {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first else {
return .zero
}
return window.safeAreaInsets
}
private func detectNotchSide(safeArea: UIEdgeInsets) -> NotchPosition {
let orientation = UIDevice.current.orientation
// Landscape left or programmatic - check left edge
if ((orientation == .landscapeLeft && safeArea.left > 0) || orientationSource == .programatically) {
return .left
}
// Landscape right - check right edge
if orientation == .landscapeRight && safeArea.right > 0 {
return .right
}
// Face up/down - use previous notch position
if orientation == .faceUp || orientation == .faceDown || orientation == .portraitUpsideDown {
return notchUpdateChange
}
// Portrait - check top edge
if orientation == .portrait && safeArea.top > 0 {
return .top
}
// No notch detected
return notchUpdateChange
}
func messageHandler(type: String, payload: Any) {
if (type == "toggleFullscreen") {
withAnimation {
isPresented.toggle()
}
changeOrientation(to: isPresented ? .landscape : .portrait)
} else if (type == "init") {
onToggleFullscreen()
} else if (type == "multibet-event") {
print(payload)
var showCustomBetslip = false
if let data = payload as? [String: Any] {
if let newSportsbookFixtureId = data["sportsbookFixtureId"] as? String {
betslipData.sportsbookFixtureId = "\(newSportsbookFixtureId)"
}
if let newSportsbookFixtureId = data["sportsbookSelectionId"] as? String {
betslipData.sportsbookSelectionId = "\(newSportsbookFixtureId)"
}
if let newSportsbookFixtureId = data["sportsbookMarketId"] as? String {
betslipData.sportsbookMarketId = "\(newSportsbookFixtureId)"
}
if let newSportsbookFixtureId = data["sportsbookMarketContext"] as? String {
betslipData.sportsbookMarketContext = "\(newSportsbookFixtureId)"
}
if let newSportsbookFixtureId = data["marketId"] as? String {
betslipData.marketId = "\(newSportsbookFixtureId)"
}
if let newDecimalPrice = data["decimalPrice"] as? Double {
betslipData.decimalPrice = newDecimalPrice
}
if let newDecimalPrice = data["stake"] as? Double {
betslipData.stake = "\(newDecimalPrice)"
}
if let command = data["command"] as? String {
if (command == "closeBetslip") {
showCustomBetslip = false
}
if (command == "openBetslip") {
showCustomBetslip = true
}
}
showBetslip = showCustomBetslip
}
} else if (type == "betslip-container-dimensions") {
if let data = payload as? [String: Any] {
if let newTop = data["top"] as? Int, let newWidth = data["width"] as? Int, let newHeight = data["height"] as? Int, let newLeft = data["left"] as? Int {
betslipCoordinates.top = newTop
betslipCoordinates.left = newLeft
betslipCoordinates.width = newWidth
betslipCoordinates.height = newHeight
}
}
}
}
func onToggleFullscreen() {
if(selectedTab == 0){
// This is needed to avoid unkown orientation value
let isPortrait = UIDevice.current.orientation.rawValue == 0 || UIDevice.current.orientation.isFlat
? UIScreen.main.bounds.size.width < UIScreen.main.bounds.size.height
: UIDevice.current.orientation == .portrait || UIDevice.current.orientation.isFlat
orientationSource = .accelerometer
if (!isPortrait && !isPresented) || (isPortrait && isPresented) {
hideKeyboard()
isPresented.toggle()
let script = """
document.querySelector(".video-container")?.classList.toggle("full-screen")
"""
webViewCoordinator.webView.evaluateJavaScript(script)
}
}
}
func updateConfiguration(configuration: VideoConfiguration) -> VideoConfiguration{
let apiKeyValue = !apiKey.isEmpty ? apiKey : customer.getApiKey(env: environment)
let customerIdValue = !customerId.isEmpty ? customerId : customer.id
configuration.fixtureId = fakeFixtureId
configuration.cgFixtureId = immersiveFixtureId
configuration.customerId = customerIdValue
configuration.apikey = apiKeyValue
configuration.user = "dummyvideocustomer"
configuration.password = "Bookmaker0987"
configuration.environment = getVideoEnvironment()
configuration.triggerOnboarding = triggerOnboarding.lowercased()
configuration.deviceId = deviceID
configuration.hashedUserEmail = sha256(userEmail)
configuration.hashedUserId = sha256(deviceID + "_" + userEmail)
configuration.culture = culture
return configuration
}
func getBaseUrl() -> URL? {
let sourceId = (source.id.isEmpty) ? "" : "\(source.id)"
return URL(string: String(format: "https://api.geniussports.com?fixtureImmersive=%@&liveStreamId=%@&showPlayerTrackingGuides=%@", immersiveFixtureId, sourceId, showPlayerTracking ? "true" : "false"))
}
func updateVideoURL(reset: Bool){
if(reset){
sources = []
source = DeliveryStream(id: "", name: "DEFAULT")
}
let configuration = updateConfiguration(configuration: VideoConfiguration())
VideoSDK().getDeliveryIds(videoConfiguration: configuration, completionHandler: { (result: Array<DeliveryStream>?, error: KotlinThrowable?) in
if let kotlinError = error {
print("Error: \(kotlinError.message ?? "")")
} else if let result = result {
sources = result
}
})
let baseURL = getBaseUrl()
webViewCoordinator.webView.loadHTMLString("Loading...", baseURL: baseURL)
VideoSDK().getVideoStream(videoConfiguration: configuration, completionHandler: { (result: String?, error: KotlinThrowable?) in
if let kotlinError = error {
print("Error: \(kotlinError.message ?? "")")
webViewCoordinator.webView.loadHTMLString("Error: \(kotlinError.message ?? "")", baseURL: baseURL)
} else if let result = result {
let htmlString = result
webViewCoordinator.webView.loadHTMLString(htmlString, baseURL: baseURL)
}
})
}
func changeOrientation(to orientation: UIInterfaceOrientationMask) {
// tell the app to change the orientation
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation))
windowScene?.keyWindow?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
orientationSource = .programatically
DispatchQueue.main.async {
notchUpdateChange = detectNotchSide(safeArea: getSafeAreaInsets())
}
}
func getVideoEnvironment() -> VideoEnvironment {
switch(environment) {
case "PROD":
return VideoEnvironment.prod
case "CI":
return VideoEnvironment.ci
default:
return VideoEnvironment.uat
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
}
}
}
enum NotchPosition: String {
case left = "left"
case right = "right"
case top = "top"
case bottom = "bottom"
case none = "No detected notch"
}
enum OrientationSource: String {
case programatically = "programatically"
case accelerometer = "accelerometer"
case none = "none"
}
7. Create Custom Betslip.
-
Create the CustomBetslip file:
This file is in charge of using the betslip data coming from the video player through the message handler created before and it can be modified as you need.
An example code could look like this:
import SwiftUI struct CustomBetslip: View { @ObservedObject var betslipData: BetslipData @Binding var showToast: Bool @Binding var showBetslip: Bool var body: some View { VStack { Text("Customer betslip") .font(.system(.title2)) Text("sportsbookFixtureId: \(betslipData.sportsbookFixtureId)") .font(.system(.subheadline)) Text("sportsbookSelectionId: \(betslipData.sportsbookSelectionId)") .font(.system(.subheadline)) Text("marketId: \(betslipData.marketId)") .font(.system(.subheadline)) Text("sportsbookMarketId: \(betslipData.sportsbookMarketId)") .font(.system(.subheadline)) Text("sportsbookMarketContext: \(betslipData.sportsbookMarketContext)") .font(.system(.subheadline)) Text("decimalPrice: \(betslipData.decimalPrice!)") .font(.system(.subheadline)) Text("stake: \(betslipData.stake)") .font(.system(.subheadline)) Button(action: { showToast = true showBetslip.toggle() }) { Text("Place bet") .padding(.vertical, 10) .foregroundStyle(Color.white) .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 60).fill(Color.blue) ) } Button(action: { showBetslip.toggle() }) { Text("Cancel") .padding(.vertical, 10) .foregroundStyle(Color.white) .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 60).fill(Color.blue) ) } } .foregroundColor(.white) .frame(width: 250) .padding(10) .background( RoundedRectangle(cornerRadius: 4).fill(Color.black.opacity(0.8)) ) } }
-
Create the CustomToast file:
import SwiftUI struct CustomToast: View { @Binding var showToast: Bool var body: some View { VStack { Spacer() HStack { Image(systemName: "checkmark.seal.fill") Text("BET PLACED!!") } .foregroundColor(.white) .padding(5) .background( RoundedRectangle(cornerRadius: 60).fill(Color.black.opacity(0.8)) ) } .padding(.bottom, 20) .onAppear() { Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in showToast = false } } } }
-
Add the CustomBetslip and CustomToast to the main view:
struct FullscreenContentView: View { @StateObject private var webViewCoordinator = WebViewCoordinator() @State private var isPresented = false @StateObject private var betslipData = BetslipData() @State private var showBetslip = false @State private var showToast = false @State private var orientationSource = OrientationSource.none var body: some View { GeometryReader { metrics in VStack(spacing: 0) { let notchUpdate = detectNotchSide(safeArea: getSafeAreaInsets()) GeometryReader { geometry in ZStack { WebViewWrapper(webView: webViewCoordinator.webView) if showBetslip { CustomBetslip(betslipData: betslipData, showToast: $showToast, showBetslip: $showBetslip) } if showToast { CustomToast(showToast: $showToast) } } } } ... } } }
-
Handle events:
Using the messageHandler
function you can listen to market related events.
func messageHandler(type: String, payload: Any) {
if (type == "toggleFullscreen") {
isPresented.toggle()
changeOrientation(to: isPresented ? .landscape : .portrait)
} else if (type == "init") {
onToggleFullscreen()
} else if (type == "multibet-event") {
print(payload)
if let data = payload as? [String: Any] {
if let newSportsbookFixtureId = data["sportsbookFixtureId"] as? String {
betslipData.sportsbookFixtureId = "\(newSportsbookFixtureId)"
}
if let newSportsbookFixtureId = data["sportsbookSelectionId"] as? String {
betslipData.sportsbookSelectionId = "\(newSportsbookFixtureId)"
}
if let newSportsbookFixtureId = data["sportsbookMarketId"] as? String {
betslipData.sportsbookMarketId = "\(newSportsbookFixtureId)"
}
if let newSportsbookFixtureId = data["sportsbookMarketContext"] as? String {
betslipData.sportsbookMarketContext = "\(newSportsbookFixtureId)"
}
if let newSportsbookFixtureId = data["marketId"] as? String {
betslipData.marketId = "\(newSportsbookFixtureId)"
}
if let newDecimalPrice = data["decimalPrice"] as? Double {
betslipData.decimalPrice = newDecimalPrice
}
if let newDecimalPrice = data["stake"] as? Double {
betslipData.stake = "\(newDecimalPrice)"
}
showBetslip = true
}
}
}
Expected payload data structure:
class BetslipData: ObservableObject {
@Published var decimalPrice: Double? = nil
@Published var command: String = ""
@Published var marketId: String = ""
@Published var sportsbookMarketContext: String = ""
@Published var sportsbookMarketId: String = ""
@Published var sportsbookFixtureId: String = ""
@Published var sportsbookSelectionId: String = ""
@Published var stake: String = ""
}
8. Handle player close.
You should also take into account handling the player close event, because if it isn't called it WILL
leak http requests. Please do it wherever it works best for your application.
You can find an example below:
// WebViewCoordinator.swift
func onDisappear() {
let script = """
if (window.GeniusLivePlayer?.player) {
window.GeniusLivePlayer.player.close()
}
"""
webView.evaluateJavaScript(script)
}
We tie it to the onDisappear of the ContentView, but it just needs to be called whenever your application requires it. In most cases it will be wherever the parent of the component disappears.
// ContentView.swift
// LoadVideoForm
.onAppear {
// Load initial web content
webViewCoordinator.messageHandler = messageHandler
updateVideoURL(reset: true)
}
.onDisappear {
webViewCoordinator.onDisappear()
}
The UIKit and Swift UI Integration were tested on iPhone 14 pro with iOS 16.4.