Skip to content

Commit

Permalink
[CHNL-11919] create native view to show web view (#212)
Browse files Browse the repository at this point in the history
* added KlaviyoWebViewController.swift

* added FileIO.swift

implemented `FileIO`

removed `internal` access control

fixed `getFileContents(...)`

* initial KlaviyoWebViewController implementation

removed config parameter

added `createWebView` comments

* added `configureSubviewConstraints()`

* added preview

added preview title

* added `createWebViewConfiguration()` method

added `createWebViewConfiguration` comments

* added `WKNavigationEvent` enum

added documentation to `WKNavigationEvent` enum

* inject URL

* added `FileIO.getFileUrl(...)` method

* added placeholder code for WKNavigationDelegate methods

* added placeholder code to handle WKScriptMessage

* refactored webView initialization

* added KlaviyoWebViewModel

* inject viewModel into viewController

* added `handleNavigationEvent` method

added // MARK

* call `handleNavigationEvent` from viewController

* added `hanldeScriptMessage` method

* added `configureScripts()`

renamed `configureScripts` and added documentation

renamed `scripts` to `loadScripts`

added TODO

* added resources to Package.swift

added bridge.js file

* added // MARK labels

added // MARK labels

added MARK

* added script execution logic

* changed script executor injection to enable callbacks

* made `loadScripts` optional

* created `KlaviyoWebViewModeling` protocol

* implemented protocol

* reorganized files

* added `return` to preview

* added swift version check

* reverted Xcode CI build versions
  • Loading branch information
ab1470 authored Oct 4, 2024
1 parent f2c76ff commit 02c559d
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
runs-on: macos-13
strategy:
matrix:
xcode: ['15.2', '15.4', '16.0']
xcode: ['14.3.1', '15.1']
config: ['debug', 'release']
steps:
- uses: actions/checkout@v3
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ let package = Package(
.target(
name: "KlaviyoUI",
dependencies: ["KlaviyoSwift"],
path: "Sources/KlaviyoUI"),
path: "Sources/KlaviyoUI",
resources: [.process("KlaviyoWebView/Resources")]),
.testTarget(
name: "KlaviyoUITests",
dependencies: [
Expand Down
136 changes: 136 additions & 0 deletions Sources/KlaviyoUI/KlaviyoWebView/KlaviyoWebViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// KlaviyoWebViewController.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 9/28/24.
//

import Combine
import UIKit
import WebKit

class KlaviyoWebViewController: UIViewController, WKUIDelegate {
var webView: WKWebView!
private let viewModel: KlaviyoWebViewModeling
private var cancellables = Set<AnyCancellable>()

// MARK: - Initializers

init(viewModel: KlaviyoWebViewModeling) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - View loading

override func loadView() {
super.loadView()

let config = createWebViewConfiguration()
webView = createWebView(with: config)
webView.navigationDelegate = self
webView.uiDelegate = self

view.addSubview(webView)

configureLoadScripts()
configureScriptEvaluator()
configureSubviewConstraints()
}

override func viewDidLoad() {
let request = URLRequest(url: viewModel.url)
webView.load(request)
}

// MARK: - WKWebView configuration

func createWebViewConfiguration() -> WKWebViewConfiguration {
let config = WKWebViewConfiguration()
// customize any WKWebViewConfiguration properties here
// ex: config.allowsInlineMediaPlayback = true
return config
}

func createWebView(with config: WKWebViewConfiguration) -> WKWebView {
let webView = WKWebView(frame: .zero, configuration: config)
// customize any WKWebView behaviors here
// ex: webView.allowsBackForwardNavigationGestures = true
return webView
}

// MARK: - Scripts

/// Configures the scripts to be injected into the website when the website loads.
func configureLoadScripts() {
guard let scriptsDict = viewModel.loadScripts else { return }

for (name, script) in scriptsDict {
webView.configuration.userContentController.addUserScript(script)
webView.configuration.userContentController.add(self, name: name)
}
}

func configureScriptEvaluator() {
viewModel.scriptSubject.sink { [weak self] script, callback in
Task { [weak self] in
do {
let result = try await self?.webView.evaluateJavaScript(script)
callback?(.success(result))
} catch {
callback?(.failure(error))
}
}
}.store(in: &cancellables)
}

// MARK: - Layout

func configureSubviewConstraints() {
webView.translatesAutoresizingMaskIntoConstraints = false
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
}

extension KlaviyoWebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
viewModel.handleNavigationEvent(.didStartProvisionalNavigation)
}

func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: any Error) {
viewModel.handleNavigationEvent(.didFailProvisionalNavigation)
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
viewModel.handleNavigationEvent(.didFinishNavigation)
}

func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: any Error) {
viewModel.handleNavigationEvent(.didFailNavigation)
}
}

extension KlaviyoWebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
viewModel.handleScriptMessage(message)
}
}

// MARK: - Previews

#if swift(>=5.9)
@available(iOS 17.0, *)
#Preview("Klaviyo.com") {
let url = URL(string: "https://www.klaviyo.com")!
let viewModel = KlaviyoWebViewModel(url: url)
return KlaviyoWebViewController(viewModel: viewModel)
}
#endif
41 changes: 41 additions & 0 deletions Sources/KlaviyoUI/KlaviyoWebView/KlaviyoWebViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// KlaviyoWebViewModel.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 9/30/24.
//

import Combine
import Foundation
import WebKit

class KlaviyoWebViewModel: KlaviyoWebViewModeling {
let url: URL
let loadScripts: [String: WKUserScript]?

/// Publishes scripts for the `WKWebView` to execute.
let scriptSubject = PassthroughSubject<(script: String, callback: ((Result<Any?, Error>) -> Void)?), Never>()

init(url: URL) {
self.url = url
loadScripts = KlaviyoWebViewModel.initializeLoadScripts()
}

private static func initializeLoadScripts() -> [String: WKUserScript] {
var scripts: [String: WKUserScript] = [:]

// TODO: initialize scripts

return scripts
}

// MARK: handle WKWebView events

func handleNavigationEvent(_ event: WKNavigationEvent) {
// TODO: handle navigation events
}

func handleScriptMessage(_ message: WKScriptMessage) {
// TODO: handle script message
}
}
23 changes: 23 additions & 0 deletions Sources/KlaviyoUI/KlaviyoWebView/KlaviyoWebViewModeling.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// KlaviyoWebViewModeling.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 10/1/24.
//

import Combine
import Foundation
import WebKit

protocol KlaviyoWebViewModeling {
var url: URL { get }

/// Scripts to be injected into the ``WKWebView`` when the website loads.
var loadScripts: [String: WKUserScript]? { get }

/// Publishes scripts for the ``WKWebView`` to execute.
var scriptSubject: PassthroughSubject<(script: String, callback: ((Result<Any?, Error>) -> Void)?), Never> { get }

func handleNavigationEvent(_ event: WKNavigationEvent)
func handleScriptMessage(_ message: WKScriptMessage)
}
6 changes: 6 additions & 0 deletions Sources/KlaviyoUI/KlaviyoWebView/Resources/Scripts/bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//
// Bridge.js
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 10/1/24.
//
26 changes: 26 additions & 0 deletions Sources/KlaviyoUI/Models/WKNavigationEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// WKNavigationEvent.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 9/30/24.
//

enum WKNavigationEvent {
/// Invoked when a main frame navigation starts.
case didStartProvisionalNavigation

/// Invoked when a server redirect is received for the main frame.
case didReceiveServerRedirectForProvisionalNavigation

/// Invoked when an error occurs while starting to load data for the main frame.
case didFailProvisionalNavigation

/// Invoked when content starts arriving for the main frame.
case didCommitNavigation

/// Invoked when a main frame navigation completes.
case didFinishNavigation

/// Invoked when an error occurs during a committed main frame navigation.
case didFailNavigation
}
35 changes: 35 additions & 0 deletions Sources/KlaviyoUI/Utilities/FileIO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// FileIO.swift
// KlaviyoSwiftUIWebView
//
// Created by Andrew Balmer on 9/27/24.
//

import Foundation

enum FileIOError: Error {
case notFound
}

enum FileIO {
static func getFileUrl(path: String, type: String) throws -> URL {
guard let fileUrl = Bundle.module.url(forResource: path, withExtension: type) else {
throw FileIOError.notFound
}

return fileUrl
}

static func getFileContents(path: String, type: String) throws -> String {
guard let path = Bundle.module.path(forResource: path, ofType: type) else {
throw FileIOError.notFound
}

do {
let contents = try String(contentsOfFile: path, encoding: String.Encoding.utf8)
return contents
} catch {
throw error
}
}
}

0 comments on commit 02c559d

Please sign in to comment.