add web view

This commit is contained in:
Xinlei(Ryan) Pan 2020-03-30 10:58:47 -04:00
parent 365b5f1bcd
commit 710adec037
4 changed files with 245 additions and 0 deletions

View File

@ -137,6 +137,8 @@
9432A79F23DB47BA00719041 /* EntryFieldContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9432A79E23DB47BA00719041 /* EntryFieldContainer.swift */; };
943784F5236B77BB006A1E82 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943784F3236B77BB006A1E82 /* GraphView.swift */; };
943784F6236B77BB006A1E82 /* GraphViewAnimationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */; };
943820842432382400B43AF3 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943820832432382400B43AF3 /* WebView.swift */; };
94382086243238D100B43AF3 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94382085243238D100B43AF3 /* WebViewModel.swift */; };
9445890C2385BCE300DE9FD4 /* ProgressBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9445890B2385BCE300DE9FD4 /* ProgressBarModel.swift */; };
9445890E2385C3F800DE9FD4 /* MultiProgressModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9445890D2385C3F800DE9FD4 /* MultiProgressModel.swift */; };
9445891F2385D2E900DE9FD4 /* CaretViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9445891E2385D2E900DE9FD4 /* CaretViewModel.swift */; };
@ -522,6 +524,8 @@
9432A79E23DB47BA00719041 /* EntryFieldContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntryFieldContainer.swift; sourceTree = "<group>"; };
943784F3236B77BB006A1E82 /* GraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = "<group>"; };
943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphViewAnimationHandler.swift; sourceTree = "<group>"; };
943820832432382400B43AF3 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
94382085243238D100B43AF3 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = "<group>"; };
9445890B2385BCE300DE9FD4 /* ProgressBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarModel.swift; sourceTree = "<group>"; };
9445890D2385C3F800DE9FD4 /* MultiProgressModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiProgressModel.swift; sourceTree = "<group>"; };
9445891E2385D2E900DE9FD4 /* CaretViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaretViewModel.swift; sourceTree = "<group>"; };
@ -1526,6 +1530,8 @@
0AA33B392398524F0067DD0F /* Toggle.swift */,
0AE98BB623FF18E9004C5109 /* ArrowModel.swift */,
0AE98BB423FF18D2004C5109 /* Arrow.swift */,
94382085243238D100B43AF3 /* WebViewModel.swift */,
943820832432382400B43AF3 /* WebView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -1902,6 +1908,7 @@
0116A4E5228B19640094F3ED /* RadioButtonSelectionHelper.swift in Sources */,
017BEB48236230DB0024EF95 /* MoleculeViewProtocol.swift in Sources */,
D2E1FADB2260D3D200AEFD8C /* MVMCoreUIDelegateObject.swift in Sources */,
94382086243238D100B43AF3 /* WebViewModel.swift in Sources */,
D27CD40E2322EEAF00C1DC07 /* TabsTableViewCell.swift in Sources */,
D224799B231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift in Sources */,
D22D1F1F220343560077CEC0 /* MVMCoreUICheckMarkView.m in Sources */,
@ -1972,6 +1979,7 @@
D29DF2A221E7AF4E003B2FB9 /* MVMCoreUIUtility.m in Sources */,
D29DF12B21E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.m in Sources */,
94C2D9A723872DA90006CF46 /* LabelAttributeColorModel.swift in Sources */,
943820842432382400B43AF3 /* WebView.swift in Sources */,
0103B84E23D7E33A009C315C /* HeadlineBodyToggleModel.swift in Sources */,
D2755D7B23689C7500485468 /* TableViewCell.swift in Sources */,
0A21DB85235E06EF00C160A2 /* MFTextField.m in Sources */,

View File

@ -0,0 +1,181 @@
//
// MVMCoreUIWebView.swift
// MVMCoreUI
//
// Created by Ryan on 8/29/19.
// Copyright © 2019 Verizon Wireless. All rights reserved.
//
import UIKit
import WebKit
@objcMembers open class WebView: View, MVMCoreUIViewConstrainingProtocol {
public let loadingSpinner = MFLoadingSpinner(frame: .zero)
var delegateObject: MVMCoreUIDelegateObject?
var webView: WKWebView?
var webViewHeight: NSLayoutConstraint?
var dynamicHeight: Bool = true
var callHandlerName: String?
override open func setupView() {
super.setupView()
let webView = createWebView(messageHandler: nil, jsScript: nil)
addSubview(webView)
NSLayoutConstraint.constraintPinSubview(toSuperview: webView)
self.webView = webView
pinSpinnerView()
}
func createWebView(messageHandler: String?, jsScript:String?) -> WKWebView {
let wkUserController = WKUserContentController()
if let messageHandlerName = messageHandler {
wkUserController.add(self, name: messageHandlerName)
}
//inital-scale is necessary to update display correct scale
let source: String = "var meta = document.createElement('meta');" +
"meta.name = 'viewport';" +
//depends on how much clint driven
"meta.content = 'initial-scale=1.0';" +
"var head = document.getElementsByTagName('head')[0];" + "head.appendChild(meta);";
let wkScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
wkUserController.addUserScript(wkScript)
//server driven addition script
if let jsScript = jsScript {
let wkScript = WKUserScript(source: jsScript, injectionTime: .atDocumentStart, forMainFrameOnly: true)
wkUserController.addUserScript(wkScript)
}
let wkConfig = WKWebViewConfiguration()
wkConfig.userContentController = wkUserController
let webView = WKWebView(frame: .zero, configuration: wkConfig)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.uiDelegate = self
webView.navigationDelegate = self
return webView
}
// MARK: - MVMCoreUIMoleculeViewProtocol
override open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? WebViewModel else { return }
self.delegateObject = delegateObject
if model.callHandler != nil || model.jsScript != nil {
/*
webView's configuration property is immutable.
In order to add call handler into webview, need to create a new webview.
callHanlder
*/
webView?.removeFromSuperview()
let webView = createWebView(messageHandler: model.callHandler, jsScript:model.jsScript)
addSubview(webView)
NSLayoutConstraint.constraintPinSubview(toSuperview: webView)
self.webView = webView
}
//init height for loading spinner
webViewHeight = webView?.heightAnchor.constraint(equalToConstant: 50)
webViewHeight?.isActive = true
if let height = model.height {
webViewHeight?.constant = height
dynamicHeight = false
}
if let url = model.url {
webView?.load(URLRequest(url: url))
} else if let htmlString = model.htmlString {
webView?.loadHTMLString(htmlString, baseURL: nil)
}
bringSubviewToFront(loadingSpinner)
//TO DO: local path?
}
func pinSpinnerView() {
addSubview(loadingSpinner)
// Setup spinner.
loadingSpinner.clipsToBounds = true
loadingSpinner.translatesAutoresizingMaskIntoConstraints = false
loadingSpinner.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
loadingSpinner.widthAnchor.constraint(equalToConstant: 50.0).isActive = true
loadingSpinner.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
loadingSpinner.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
loadingSpinner.resumeSpinner()
}
}
// MARK: - WKUIDelegate
extension WebView : WKUIDelegate {
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// hide loading
loadingSpinner.pause()
if !dynamicHeight {
return
}
//update webview's heigth when webview is ready
webView.evaluateJavaScript("document.readyState", completionHandler: { [weak self] (result, error) in
if result == nil || error != nil {
return
}
webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (result, error) in
guard let height = result as? CGFloat else { return }
self?.webViewHeight?.constant = height
self?.delegateObject?.moleculeDelegate?.moleculeLayoutUpdated(self!)
})
})
}
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
// show loading
loadingSpinner.resumeSpinner()
}
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
// hide loading
loadingSpinner.pause()
}
}
// MARK: - WKNavigationDelegate
extension WebView : WKNavigationDelegate {
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
//validate request url
//all validated link should be open in safari
if (navigationAction.navigationType == .linkActivated), let urlString = navigationAction.request.url?.absoluteString.removingPercentEncoding, !urlString.contains("#") {
MVMCoreActionHandler.shared()?.openURL(inWebView: navigationAction.request.url, actionInformation: nil, additionalData: nil, delegateObject: nil)
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
}
// MARK: - WKScriptMessageHandler
extension WebView: WKScriptMessageHandler {
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == callHandlerName, let text = message.body as? String {
/*
receiving JavaScript func webkit.messageHandlers.{callHandler}.postMessage(body);
if body is dictionary string
MVMCoreActionHanlder handleAction
*/
if let data = text.data(using: .utf8) {
do {
let actionMap = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: nil, delegateObject: self.delegateObject)
} catch {
// if post message is not dictionary string
print(error.localizedDescription)
}
}
}
}
}

View File

@ -0,0 +1,55 @@
//
// WebviewModel.swift
// MVMCoreUI
//
// Created by Kruthika KP on 09/03/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
@objcMembers public class WebViewModel: MoleculeModelProtocol {
public static var identifier: String = "webview"
public var moleculeName: String = WebViewModel.identifier
public var backgroundColor: Color?
public var url: URL?
public var htmlString: String?
public var height: CGFloat?
public var jsScript: String?
public var callHandler: String?
public var buttonMap: [String: ButtonModel]?
private enum CodingKeys: String, CodingKey{
case moleculeName
case backgroundColor
case url
case htmlString
case height
case buttonMap
case jsScript
case callHandler
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
url = try typeContainer.decodeIfPresent(URL.self, forKey: .url)
htmlString = try typeContainer.decodeIfPresent(String.self, forKey: .htmlString)
height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height)
buttonMap = try typeContainer.decodeIfPresent([String: ButtonModel].self, forKey: .buttonMap)
jsScript = try typeContainer.decodeIfPresent(String.self, forKey: .jsScript)
callHandler = try typeContainer.decodeIfPresent(String.self, forKey: .callHandler)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encodeIfPresent(url, forKey: .url)
try container.encodeIfPresent(htmlString, forKey: .htmlString)
try container.encodeIfPresent(height, forKey: .height)
try container.encodeIfPresent(jsScript, forKey: .jsScript)
try container.encodeIfPresent(callHandler, forKey: .callHandler)
}
}

View File

@ -80,6 +80,7 @@ import Foundation
MoleculeObjectMapping.shared()?.register(viewClass: Arrow.self, viewModelClass: ArrowModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: RadioButton.self, viewModelClass: RadioButtonModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: RadioButtonLabel.self, viewModelClass: RadioButtonLabelModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: WebView.self, viewModelClass: WebViewModel.self)
// Horizontal Combination Molecules
MoleculeObjectMapping.shared()?.register(viewClass: StringAndMoleculeView.self, viewModelClass: StringAndMoleculeModel.self)