Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .github/workflows/build-test-and-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,14 @@ jobs:

- name: Build
run: |
set -uexo pipefail
set -uxo pipefail
device_type=${{ matrix.device-type }}
set +e
deviceid=$(xcrun simctl list devices $device_type available | grep -v -- -- | tail -n 1 | grep -oE '[0-9A-F\-]{36}')
if [ $? -eq 0 ]; then
(
set -e

buildtarget () {
# Use the same derived data path as DocC compilation so that we don't duplicate work.
xcodebuild -derivedDataPath /tmp/data -skipMacroValidation -scheme "$1" -destination "id=$deviceid" build | xcbeautify --renderer github-actions
Expand Down Expand Up @@ -116,6 +118,7 @@ jobs:
)
else
echo "No $device_type simulators found" >&2
false
fi

- name: Extract UIKitBackend symbol graphs
Expand Down
16 changes: 7 additions & 9 deletions Examples/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Sources/SwiftCrossUI/Views/WebView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation

@available(tvOS, unavailable)
public struct WebView: ElementaryView {
@State var currentURL: URL?
@Binding var url: URL
Expand Down
87 changes: 53 additions & 34 deletions Sources/UIKitBackend/UIKitBackend+Control.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ final class TextFieldWidget: WrapperWidget<UITextField>, UITextFieldDelegate {

final class TextEditorWidget: WrapperWidget<UITextView>, UITextViewDelegate {
var onChange: ((String) -> Void)?
var isEditable: Bool = true {
didSet {
#if os(tvOS)
if !isEditable {
child.resignFirstResponder()
}
#else
child.isEditable = isEditable
#endif
}
}

init() {
super.init(child: UITextView())
Expand All @@ -68,6 +79,10 @@ final class TextEditorWidget: WrapperWidget<UITextView>, UITextViewDelegate {
func textViewDidChange(_: UITextView) {
onChange?(child.text ?? "")
}

func textViewShouldBeginEditing(_: UITextView) -> Bool {
return isEditable
}
}

#if os(tvOS)
Expand Down Expand Up @@ -172,32 +187,36 @@ final class TappableWidget: ContainerWidget {

@available(tvOS, unavailable)
final class HoverableWidget: ContainerWidget {
private var hoverGestureRecognizer: UIHoverGestureRecognizer?

var hoverChangesHandler: ((Bool) -> Void)? {
didSet {
if hoverChangesHandler != nil && hoverGestureRecognizer == nil {
let gestureRecognizer = UIHoverGestureRecognizer(
target: self,
action: #selector(hoveringChanged(_:)))
child.view.addGestureRecognizer(gestureRecognizer)
self.hoverGestureRecognizer = gestureRecognizer
} else if hoverChangesHandler == nil, let hoverGestureRecognizer {
child.view.removeGestureRecognizer(hoverGestureRecognizer)
self.hoverGestureRecognizer = nil
// So much as attempting to reference UIHoverGestureRecognizer here results in a linker error on tvOS.
#if !os(tvOS)
private var hoverGestureRecognizer: UIHoverGestureRecognizer?

var hoverChangesHandler: ((Bool) -> Void)? {
didSet {
if hoverChangesHandler != nil && hoverGestureRecognizer == nil {
let gestureRecognizer = UIHoverGestureRecognizer(
target: self,
action: #selector(hoveringChanged(_:)))
child.view.addGestureRecognizer(gestureRecognizer)
self.hoverGestureRecognizer = gestureRecognizer
} else if hoverChangesHandler == nil, let hoverGestureRecognizer {
child.view.removeGestureRecognizer(hoverGestureRecognizer)
self.hoverGestureRecognizer = nil
}
}
}
}

@objc
func hoveringChanged(_ recognizer: UIHoverGestureRecognizer) {
switch recognizer.state {
case .began: hoverChangesHandler?(true)
case .ended: hoverChangesHandler?(false)
default: break
@objc
func hoveringChanged(_ recognizer: UIHoverGestureRecognizer) {
switch recognizer.state {
case .began: hoverChangesHandler?(true)
case .ended: hoverChangesHandler?(false)
default: break
}
}
}
#endif
}

@available(tvOS, unavailable)
final class SliderWidget: WrapperWidget<UISlider> {
var onChange: ((Double) -> Void)?
Expand Down Expand Up @@ -332,7 +351,7 @@ extension UIKitBackend {
) {
let textEditorWidget = textEditor as! TextEditorWidget

textEditorWidget.child.isEditable = environment.isEnabled
textEditorWidget.isEditable = environment.isEnabled
textEditorWidget.child.font = environment.resolvedFont.uiFont
textEditorWidget.child.textColor = UIColor(color: environment.suggestedForegroundColor)
textEditorWidget.onChange = onChange
Expand Down Expand Up @@ -448,20 +467,20 @@ extension UIKitBackend {
}
}

public func createHoverTarget(wrapping child: Widget) -> Widget {
HoverableWidget(child: child)
}
#if os(iOS) || os(visionOS) || targetEnvironment(macCatalyst)
public func createHoverTarget(wrapping child: Widget) -> Widget {
HoverableWidget(child: child)
}

public func updateHoverTarget(
_ hoverTarget: any WidgetProtocol,
environment: EnvironmentValues,
action: @escaping (Bool) -> Void
) {
let wrapper = hoverTarget as! HoverableWidget
wrapper.hoverChangesHandler = action
}
public func updateHoverTarget(
_ hoverTarget: any WidgetProtocol,
environment: EnvironmentValues,
action: @escaping (Bool) -> Void
) {
let wrapper = hoverTarget as! HoverableWidget
wrapper.hoverChangesHandler = action
}

#if os(iOS) || os(visionOS) || targetEnvironment(macCatalyst)
public func createSlider() -> Widget {
SliderWidget()
}
Expand Down
68 changes: 35 additions & 33 deletions Sources/UIKitBackend/UIKitBackend+WebView.swift
Original file line number Diff line number Diff line change
@@ -1,43 +1,45 @@
import SwiftCrossUI
import WebKit
#if !os(tvOS)
import SwiftCrossUI
import WebKit

extension UIKitBackend {
public func createWebView() -> Widget {
WebViewWidget()
}
extension UIKitBackend {
public func createWebView() -> Widget {
WebViewWidget()
}

public func updateWebView(
_ webView: Widget,
environment: EnvironmentValues,
onNavigate: @escaping (URL) -> Void
) {
let webView = webView as! WebViewWidget
webView.onNavigate = onNavigate
}
public func updateWebView(
_ webView: Widget,
environment: EnvironmentValues,
onNavigate: @escaping (URL) -> Void
) {
let webView = webView as! WebViewWidget
webView.onNavigate = onNavigate
}

public func navigateWebView(_ webView: Widget, to url: URL) {
let webView = webView as! WebViewWidget
let request = URLRequest(url: url)
webView.child.load(request)
public func navigateWebView(_ webView: Widget, to url: URL) {
let webView = webView as! WebViewWidget
let request = URLRequest(url: url)
webView.child.load(request)
}
}
}

/// A wrapper for WKWebView. Acts as the web view's delegate as well.
final class WebViewWidget: WrapperWidget<WKWebView>, WKNavigationDelegate {
var onNavigate: ((URL) -> Void)?
/// A wrapper for WKWebView. Acts as the web view's delegate as well.
final class WebViewWidget: WrapperWidget<WKWebView>, WKNavigationDelegate {
var onNavigate: ((URL) -> Void)?

init() {
super.init(child: WKWebView())
init() {
super.init(child: WKWebView())

child.navigationDelegate = self
}

func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
guard let url = webView.url else {
print("warning: Web view has no URL")
return
child.navigationDelegate = self
}

onNavigate?(url)
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
guard let url = webView.url else {
print("warning: Web view has no URL")
return
}

onNavigate?(url)
}
}
}
#endif
Loading