Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SwiftCrossUI
struct GreetingGeneratorApp: App {
@State var name = ""
@State var greetings: [String] = []
@State var isGreetingSelectable = false

var body: some Scene {
WindowGroup("Greeting Generator") {
Expand All @@ -26,9 +27,11 @@ struct GreetingGeneratorApp: App {
}
}

Toggle("Selectable Greeting", active: $isGreetingSelectable)
if let latest = greetings.last {
Text(latest)
.padding(.top, 5)
.textSelectionEnabled(isGreetingSelectable)

if greetings.count > 1 {
Text("History:")
Expand Down
1 change: 1 addition & 0 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ public final class AppKitBackend: AppBackend {
) {
let field = textView as! NSTextField
field.attributedStringValue = Self.attributedString(for: content, in: environment)
field.isSelectable = environment.isTextSelectionEnabled
}

public func createButton() -> Widget {
Expand Down
2 changes: 1 addition & 1 deletion Sources/GtkBackend/GtkBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ public final class GtkBackend: AppBackend {
case .trailing:
Justification.right
}

textView.selectable = environment.isTextSelectionEnabled
textView.css.clear()
textView.css.set(properties: Self.cssProperties(for: environment))
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftCrossUI/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public struct EnvironmentValues {
/// The style of toggle to use.
public var toggleStyle: ToggleStyle

/// Whether the text should be selectable. Set by ``View/textSelectionEnabled(_:)``.
public var isTextSelectionEnabled: Bool

// Backing storage for extensible subscript
private var extraValues: [ObjectIdentifier: Any]

Expand Down Expand Up @@ -208,6 +211,7 @@ public struct EnvironmentValues {
toggleStyle = .button
isEnabled = true
scrollDismissesKeyboardMode = .automatic
isTextSelectionEnabled = false
}

/// Returns a copy of the environment with the specified property set to the
Expand Down
10 changes: 10 additions & 0 deletions Sources/SwiftCrossUI/Views/Modifiers/TextSelectionModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
extension View {
/// Set selectability of contained text.
public func textSelectionEnabled(_ isEnabled: Bool = true) -> some View {
EnvironmentModifier(
self,
modification: { environment in
environment.with(\.isTextSelectionEnabled, isEnabled)
})
}
}
70 changes: 68 additions & 2 deletions Sources/UIKitBackend/UIKitBackend+Passive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
}

public func createTextView() -> Widget {
let widget = WrapperWidget<UILabel>()
let widget = WrapperWidget<OptionallySelectableLabel>()
widget.child.numberOfLines = 0
return widget
}
Expand All @@ -46,12 +46,13 @@
content: String,
environment: EnvironmentValues
) {
let wrapper = textView as! WrapperWidget<UILabel>
let wrapper = textView as! WrapperWidget<OptionallySelectableLabel>
wrapper.child.overrideUserInterfaceStyle = environment.colorScheme.userInterfaceStyle
wrapper.child.attributedText = UIKitBackend.attributedString(
text: content,
environment: environment
)
wrapper.child.isSelectable = environment.isTextSelectionEnabled
}

public func size(
Expand Down Expand Up @@ -104,3 +105,68 @@
wrapper.child.image = .init(ciImage: ciImage)
}
}

// Inspired by https://medium.com/kinandcartacreated/making-uilabel-accessible-5f3d5c342df4
// Thank you to Sam Dods for the base idea
final class OptionallySelectableLabel: UILabel {
var isSelectable: Bool = false

override init(frame: CGRect) {
super.init(frame: frame)
setupTextSelection()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupTextSelection()
}

override var canBecomeFirstResponder: Bool {
isSelectable
}

private func setupTextSelection() {
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
addGestureRecognizer(longPress)
isUserInteractionEnabled = true
}

@objc private func didLongPress(_ gesture: UILongPressGestureRecognizer) {
guard
isSelectable,
gesture.state == .began,
let text = self.attributedText?.string,
!text.isEmpty
else {
return
}
window?.endEditing(true)
guard becomeFirstResponder() else { return }

let menu = UIMenuController.shared

Check warning on line 146 in Sources/UIKitBackend/UIKitBackend+Passive.swift

View workflow job for this annotation

GitHub Actions / uikit (Vision)

'UIMenuController' was deprecated in visionOS 1.0: UIMenuController is deprecated. Use UIEditMenuInteraction instead.
if !menu.isMenuVisible {
menu.showMenu(from: self, rect: textRect())
}
}

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(copy(_:))
}

private func textRect() -> CGRect {
let inset: CGFloat = -4
return textRect(forBounds: bounds, limitedToNumberOfLines: numberOfLines)
.insetBy(dx: inset, dy: inset)
}

private func cancelSelection() {
let menu = UIMenuController.shared

Check warning on line 163 in Sources/UIKitBackend/UIKitBackend+Passive.swift

View workflow job for this annotation

GitHub Actions / uikit (Vision)

'UIMenuController' was deprecated in visionOS 1.0: UIMenuController is deprecated. Use UIEditMenuInteraction instead.
menu.hideMenu(from: self)
}

@objc override func copy(_ sender: Any?) {
cancelSelection()
let board = UIPasteboard.general
board.string = text
}
}
1 change: 1 addition & 0 deletions Sources/WinUIBackend/WinUIBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ public final class WinUIBackend: AppBackend {
) {
let block = textView as! TextBlock
block.text = content
block.isTextSelectionEnabled = environment.isTextSelectionEnabled
missing("font design handling (monospace vs normal)")
environment.apply(to: block)
}
Expand Down
Loading