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-13251] dismiss webview on border tap #232

Closed
wants to merge 12 commits into from
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// PreviewGridViewController.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 10/23/24.
//

#if DEBUG
import Foundation
import UIKit

/// A sample grid view with randomly-colored tiles for internal Xcode preview purposes only.
class PreviewGridViewController: UIViewController, UICollectionViewDataSource {
var collectionView: UICollectionView!

override func viewDidLoad() {
super.viewDidLoad()

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.4))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.interItemSpacing = .flexible(12)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 12
section.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)

let layout = UICollectionViewCompositionalLayout(section: section)

collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
collectionView.dataSource = self

view.addSubview(collectionView)

// Disable autoresizing mask translation for Auto Layout
collectionView.translatesAutoresizingMaskIntoConstraints = false

// Set up constraints using safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
}

// MARK: UICollectionViewDataSource

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
12
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = UIColor(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1),
alpha: 1.0)
cell.layer.cornerRadius = 12.0
return cell
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// PreviewTabViewController.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 10/23/24.
//

#if DEBUG
import Foundation
import UIKit

/// A sample tab view for internal Xcode preview purposes only.
class PreviewTabViewController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()

let firstVC = PreviewGridViewController()
let secondVC = UIViewController()
let thirdVC = UIViewController()

firstVC.title = "Browse"

firstVC.tabBarItem = UITabBarItem(title: "Browse", image: UIImage(systemName: "house"), tag: 0)
secondVC.tabBarItem = UITabBarItem(title: "Search", image: UIImage(systemName: "magnifyingglass"), tag: 1)
thirdVC.tabBarItem = UITabBarItem(title: "Profile", image: UIImage(systemName: "person"), tag: 2)

viewControllers = [
UINavigationController(rootViewController: firstVC),
UINavigationController(rootViewController: secondVC),
UINavigationController(rootViewController: thirdVC)
]

firstVC.navigationController?.navigationBar.prefersLargeTitles = true
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ class KlaviyoWebViewController: UIViewController, WKUIDelegate {

view.addSubview(webView)

configureSubviewConstraints()
configureLoadScripts()
configureScriptEvaluator()
configureSubviewConstraints()
}

override func viewDidLoad() {
Expand Down
6 changes: 6 additions & 0 deletions Sources/KlaviyoUI/KlaviyoWebView/KlaviyoWebViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,10 @@ class KlaviyoWebViewModel: KlaviyoWebViewModeling {
func handleScriptMessage(_ message: WKScriptMessage) {
// TODO: handle script message
}

// MARK: handle user events

func dismiss() {
// TODO: handle dismiss
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ protocol KlaviyoWebViewModeling {

func handleNavigationEvent(_ event: WKNavigationEvent)
func handleScriptMessage(_ message: WKScriptMessage)
func dismiss()
}
32 changes: 32 additions & 0 deletions Sources/KlaviyoUI/KlaviyoWebView/KlaviyoWebWrapperStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// KlaviyoWebWrapperStyle.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 10/28/24.
//

import UIKit

public struct KlaviyoWebWrapperStyle {
public enum BackgroundStyle {
case blurred(effect: UIBlurEffect.Style)
case tinted(color: UIColor = .black, opacity: Float)
}

var backgroundStyle: BackgroundStyle
var insets: NSDirectionalEdgeInsets
var cornerRadius: CGFloat
var shadowStyle: ShadowStyle?
}

extension KlaviyoWebWrapperStyle {
static var `default` = Self(
backgroundStyle: .blurred(effect: .systemUltraThinMaterialDark),
insets: NSDirectionalEdgeInsets(top: 24, leading: 36, bottom: 24, trailing: 36),
cornerRadius: 16.0,
shadowStyle: .init(
color: UIColor.black.cgColor,
opacity: 0.5,
offset: CGSize(width: 5, height: 5),
radius: 8))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//
// File.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 10/22/24.
//

import Foundation
import UIKit

public class KlaviyoWebWrapperViewController: UIViewController {
// MARK: - Properties

let viewModel: KlaviyoWebViewModeling
let style: KlaviyoWebWrapperStyle

private lazy var dismissGestureRecognizer: UITapGestureRecognizer = {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDismissGesture))
tapRecognizer.numberOfTapsRequired = 1
tapRecognizer.numberOfTouchesRequired = 1
return tapRecognizer
}()

// MARK: - Subviews

private lazy var blurEffectView: UIVisualEffectView? = {
guard case let .blurred(effect) = style.backgroundStyle else { return nil }

let blurEffect = UIBlurEffect(style: effect)
let blurEffectView = UIVisualEffectView(effect: blurEffect)

blurEffectView.addGestureRecognizer(dismissGestureRecognizer)

return blurEffectView
}()

private lazy var tintView: UIView? = {
guard case let .tinted(color, opacity) = style.backgroundStyle else { return nil }

let tintView = UIView()
tintView.backgroundColor = color
tintView.layer.opacity = opacity

tintView.addGestureRecognizer(dismissGestureRecognizer)

return tintView
}()

private lazy var shadowContainerView: UIView? = {
guard let shadowProperties = style.shadowStyle else { return nil }

let shadowContainerView = UIView()

shadowContainerView.layer.shadowColor = shadowProperties.color
shadowContainerView.layer.shadowOpacity = shadowProperties.opacity
shadowContainerView.layer.shadowOffset = shadowProperties.offset
shadowContainerView.layer.shadowRadius = shadowProperties.radius

shadowContainerView.layer.masksToBounds = false

return shadowContainerView
}()

private lazy var webViewController: KlaviyoWebViewController = {
let webViewController = KlaviyoWebViewController(viewModel: viewModel)
guard let webView = webViewController.view else { return webViewController }
webView.layer.cornerRadius = 16
webView.layer.masksToBounds = true

return webViewController
}()

// MARK: - View Initialization

init(viewModel: KlaviyoWebViewModeling, style: KlaviyoWebWrapperStyle = .default) {
self.viewModel = viewModel
self.style = style

super.init(nibName: nil, bundle: nil)
}

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

override public func viewDidLoad() {
super.viewDidLoad()
loadSubviews()
}

func loadSubviews() {
if let blurEffectView {
view.addSubview(blurEffectView)
blurEffectView.pin(to: view)
}

if let tintView {
view.addSubview(tintView)
tintView.pin(to: view)
}

guard let webView = webViewController.view else { return }
let webViewInsets = style.insets

if let shadowContainerView {
view.addSubview(shadowContainerView)
shadowContainerView.addSubview(webView)

shadowContainerView.pin(to: view.safeAreaLayoutGuide, insets: webViewInsets)
webView.pin(to: shadowContainerView)

addChild(webViewController)
webViewController.didMove(toParent: self)
} else {
view.addSubview(webView)

webView.pin(to: view.safeAreaLayoutGuide, insets: webViewInsets)

addChild(webViewController)
webViewController.didMove(toParent: self)
}
}

// MARK: - user interactions

@objc private func handleDismissGesture() {
viewModel.dismiss()
}
}

// MARK: - Previews

#if DEBUG
func createKlaviyoWebPreview(url: URL, style: KlaviyoWebWrapperStyle) -> UIViewController {
let viewModel = KlaviyoWebViewModel(url: url)
let viewController = KlaviyoWebWrapperViewController(viewModel: viewModel, style: style)

// Add a dummy view in the background to preview what the KlaviyoWebWrapperViewController
// might look like when it's displayed on top of a view in an app.
let childViewController = PreviewTabViewController()
viewController.view.addSubview(childViewController.view)
viewController.view.sendSubviewToBack(childViewController.view)
viewController.addChild(childViewController)
childViewController.didMove(toParent: viewController)

return viewController
}
#endif

#if swift(>=5.9)
@available(iOS 17.0, *)
#Preview("Default style") {
let url = URL(string: "https://www.google.com")!
return createKlaviyoWebPreview(url: url, style: .default)
}

@available(iOS 17.0, *)
#Preview("Tinted background") {
let url = URL(string: "https://www.google.com")!
let style = KlaviyoWebWrapperStyle(
backgroundStyle: .tinted(opacity: 0.6),
insets: NSDirectionalEdgeInsets(top: 24, leading: 36, bottom: 24, trailing: 36),
cornerRadius: 24,
shadowStyle: .default)

return createKlaviyoWebPreview(url: url, style: style)
}
#endif
23 changes: 23 additions & 0 deletions Sources/KlaviyoUI/Styles/ShadowStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// ShadowStyle.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 10/28/24.
//

import UIKit

public struct ShadowStyle {
let color: CGColor
let opacity: Float
let offset: CGSize
let radius: CGFloat
}

extension ShadowStyle {
public static var `default`: Self = .init(
color: UIColor.black.cgColor,
opacity: 0.5,
offset: CGSize(width: 5, height: 5),
radius: 8)
}
36 changes: 36 additions & 0 deletions Sources/KlaviyoUI/Utilities/UIView+Ext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// UIView+Ext.swift
// klaviyo-swift-sdk
//
// Created by Andrew Balmer on 10/25/24.
//

import UIKit

extension UIView {
/// Pins the view to the edges of the parent view.
/// - Parameter parentView: The parent view to pin this view to.
func pin(to parentView: UIView) {
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
topAnchor.constraint(equalTo: parentView.topAnchor),
bottomAnchor.constraint(equalTo: parentView.bottomAnchor)
])
}

/// Pins the view to the edges of the given safe area with optional insets.
/// - Parameters:
/// - safeArea: The UILayoutGuide (usually `view.safeAreaLayoutGuide`) to pin the view to.
/// - insets: Optional insets for each side, default is `.zero`.
func pin(to safeArea: UILayoutGuide, insets: NSDirectionalEdgeInsets = .zero) {
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topAnchor.constraint(equalTo: safeArea.topAnchor, constant: insets.top),
leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: insets.leading),
bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -insets.bottom),
trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -insets.trailing)
])
}
}
Loading