Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CHNL-11919] create native view to show web view #212

Merged
merged 29 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cc62d39
added KlaviyoWebViewController.swift
ab1470 Sep 28, 2024
6448ced
added FileIO.swift
ab1470 Sep 28, 2024
56bc924
initial KlaviyoWebViewController implementation
ab1470 Sep 28, 2024
f6127a8
added `configureSubviewConstraints()`
ab1470 Sep 28, 2024
2cf0623
added preview
ab1470 Sep 30, 2024
b259f39
added `createWebViewConfiguration()` method
ab1470 Sep 30, 2024
ecb58ad
added `WKNavigationEvent` enum
ab1470 Sep 30, 2024
7c6e683
inject URL
ab1470 Sep 30, 2024
91dc253
added `FileIO.getFileUrl(...)` method
ab1470 Oct 1, 2024
c79b4c3
added placeholder code for WKNavigationDelegate methods
ab1470 Oct 1, 2024
05c53d0
added placeholder code to handle WKScriptMessage
ab1470 Oct 1, 2024
3cfe28a
refactored webView initialization
ab1470 Oct 1, 2024
b4e1e93
added KlaviyoWebViewModel
ab1470 Oct 1, 2024
88e2c1d
inject viewModel into viewController
ab1470 Oct 1, 2024
1787eaa
added `handleNavigationEvent` method
ab1470 Oct 1, 2024
496bf68
call `handleNavigationEvent` from viewController
ab1470 Oct 1, 2024
d85b440
added `hanldeScriptMessage` method
ab1470 Oct 1, 2024
7b0872c
added `configureScripts()`
ab1470 Oct 1, 2024
a257b0a
added resources to Package.swift
ab1470 Oct 1, 2024
59d65b1
added // MARK labels
ab1470 Oct 1, 2024
f51a87f
added script execution logic
ab1470 Oct 1, 2024
30e1a3a
changed script executor injection to enable callbacks
ab1470 Oct 1, 2024
2c0f6c3
made `loadScripts` optional
ab1470 Oct 1, 2024
a19fd7a
created `KlaviyoWebViewModeling` protocol
ab1470 Oct 1, 2024
80eabac
implemented protocol
ab1470 Oct 1, 2024
b149527
reorganized files
ab1470 Oct 1, 2024
1ec0bfd
added `return` to preview
ab1470 Oct 2, 2024
b0a5d67
added swift version check
ab1470 Oct 3, 2024
ba283d5
reverted Xcode CI build versions
ab1470 Oct 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
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
}
}
}
Loading