I've been trying to create a multiline TextField in SwiftUI, but I can't figure out how.
This is the code I currently have:
struct EditorTextView : View {
#Binding var text: String
var body: some View {
TextField($text)
.lineLimit(4)
.multilineTextAlignment(.leading)
.frame(minWidth: 100, maxWidth: 200, minHeight: 100, maxHeight: .infinity, alignment: .topLeading)
}
}
#if DEBUG
let sampleText = """
Very long line 1
Very long line 2
Very long line 3
Very long line 4
"""
struct EditorTextView_Previews : PreviewProvider {
static var previews: some View {
EditorTextView(text: .constant(sampleText))
.previewLayout(.fixed(width: 200, height: 200))
}
}
#endif
But this is the output:
iOS 16 - beta
TextFields can be configured to expand vertically using the new axis parameter. Also it takes the lineLimit modifier to limit the lines in the given range:
TextField("Title", text: $text, axis: .vertical)
.lineLimit(5...10)
But the lineLimit modifier now also supports more advanced behaviors, like reserving a minimum amount of space and expanding as more content is added, and then scrolling once the content exceeds the upper limit
iOS 14 and 15 - Native SwiftUI
It is called TextEditor
struct ContentView: View {
#State var text: String = "Multiline \ntext \nis called \nTextEditor"
var body: some View {
TextEditor(text: $text)
}
}
🎁 Dynamic growing height:
If you want it to grow as you type, embed it in a ZStack with a Text like this:
iOS 13 - Using UITextView
you can use the native UITextView right in the SwiftUI code with this struct:
struct TextView: UIViewRepresentable {
typealias UIViewType = UITextView
var configuration = { (view: UIViewType) in }
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIViewType {
UIViewType()
}
func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<Self>) {
configuration(uiView)
}
}
Usage
struct ContentView: View {
var body: some View {
TextView() {
$0.textColor = .red
// Any other setup you like
}
}
}
💡 Advantages:
Support for iOS 13
Shared with the legacy code
Tested for years in UIKit
Fully customizable
All other benefits of the original UITextView
Ok, I started with #sas approach, but needed it really look&feel as multi-line text field with content fit, etc. Here is what I've got. Hope it will be helpful for somebody else... Used Xcode 11.1.
Provided custom MultilineTextField has:
1. content fit
2. autofocus
3. placeholder
4. on commit
import SwiftUI
import UIKit
fileprivate struct UITextViewWrapper: UIViewRepresentable {
typealias UIViewType = UITextView
#Binding var text: String
#Binding var calculatedHeight: CGFloat
var onDone: (() -> Void)?
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
let textField = UITextView()
textField.delegate = context.coordinator
textField.isEditable = true
textField.font = UIFont.preferredFont(forTextStyle: .body)
textField.isSelectable = true
textField.isUserInteractionEnabled = true
textField.isScrollEnabled = false
textField.backgroundColor = UIColor.clear
if nil != onDone {
textField.returnKeyType = .done
}
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
}
fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if result.wrappedValue != newSize.height {
DispatchQueue.main.async {
result.wrappedValue = newSize.height // !! must be called asynchronously
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
var calculatedHeight: Binding<CGFloat>
var onDone: (() -> Void)?
init(text: Binding<String>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
self.text = text
self.calculatedHeight = height
self.onDone = onDone
}
func textViewDidChange(_ uiView: UITextView) {
text.wrappedValue = uiView.text
UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let onDone = self.onDone, text == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
}
}
struct MultilineTextField: View {
private var placeholder: String
private var onCommit: (() -> Void)?
#Binding private var text: String
private var internalText: Binding<String> {
Binding<String>(get: { self.text } ) {
self.text = $0
self.showingPlaceholder = $0.isEmpty
}
}
#State private var dynamicHeight: CGFloat = 100
#State private var showingPlaceholder = false
init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
self._text = text
self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
}
var body: some View {
UITextViewWrapper(text: self.internalText, calculatedHeight: $dynamicHeight, onDone: onCommit)
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
.background(placeholderView, alignment: .topLeading)
}
var placeholderView: some View {
Group {
if showingPlaceholder {
Text(placeholder).foregroundColor(.gray)
.padding(.leading, 4)
.padding(.top, 8)
}
}
}
}
#if DEBUG
struct MultilineTextField_Previews: PreviewProvider {
static var test:String = ""//some very very very long description string to be initially wider than screen"
static var testBinding = Binding<String>(get: { test }, set: {
// print("New value: \($0)")
test = $0 } )
static var previews: some View {
VStack(alignment: .leading) {
Text("Description:")
MultilineTextField("Enter some text here", text: testBinding, onCommit: {
print("Final text: \(test)")
})
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.black))
Text("Something static here...")
Spacer()
}
.padding()
}
}
#endif
Update: While Xcode11 beta 4 now does support TextView, I've found that wrapping a UITextView is still be best way to get editable multiline text to work. For instance, TextView has display glitches where text does not appear properly inside the view.
Original (beta 1) answer:
For now, you could wrap a UITextView to create a composable View:
import SwiftUI
import Combine
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData, Never>()
var text = "" {
didSet {
didChange.send(self)
}
}
init(text: String) {
self.text = text
}
}
struct MultilineTextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isScrollEnabled = true
view.isEditable = true
view.isUserInteractionEnabled = true
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
struct ContentView : View {
#State private var selection = 0
#EnvironmentObject var userData: UserData
var body: some View {
TabbedView(selection: $selection){
MultilineTextView(text: $userData.text)
.tabItemLabel(Image("first"))
.tag(0)
Text("Second View")
.font(.title)
.tabItemLabel(Image("second"))
.tag(1)
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(UserData(
text: """
Some longer text here
that spans a few lines
and runs on.
"""
))
}
}
#endif
With a Text() you can achieve this using .lineLimit(nil), and the documentation suggests this should work for TextField() too. However, I can confirm this does not currently work as expected.
I suspect a bug - would recommend filing a report with Feedback Assistant. I have done this and the ID is FB6124711.
EDIT: Update for iOS 14: use the new TextEditor instead.
This wraps UITextView in Xcode Version 11.0 beta 6 (still working at Xcode 11 GM seed 2):
import SwiftUI
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
Text("text is: \(text)")
TextView(
text: $text
)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
}
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.font = UIFont(name: "HelveticaNeue", size: 15)
myTextView.isScrollEnabled = true
myTextView.isEditable = true
myTextView.isUserInteractionEnabled = true
myTextView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
print("text now: \(String(describing: textView.text!))")
self.parent.text = textView.text
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#Meo Flute's answer is great! But it doesn't work for multistage text input.
And combined with #Asperi's answer, here is the fixed for that and I also added the support for placeholder just for fun!
struct TextView: UIViewRepresentable {
var placeholder: String
#Binding var text: String
var minHeight: CGFloat
#Binding var calculatedHeight: CGFloat
init(placeholder: String, text: Binding<String>, minHeight: CGFloat, calculatedHeight: Binding<CGFloat>) {
self.placeholder = placeholder
self._text = text
self.minHeight = minHeight
self._calculatedHeight = calculatedHeight
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
// Decrease priority of content resistance, so content would not push external layout set in SwiftUI
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.isScrollEnabled = false
textView.isEditable = true
textView.isUserInteractionEnabled = true
textView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)
// Set the placeholder
textView.text = placeholder
textView.textColor = UIColor.lightGray
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.text = self.text
recalculateHeight(view: textView)
}
func recalculateHeight(view: UIView) {
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if minHeight < newSize.height && $calculatedHeight.wrappedValue != newSize.height {
DispatchQueue.main.async {
self.$calculatedHeight.wrappedValue = newSize.height // !! must be called asynchronously
}
} else if minHeight >= newSize.height && $calculatedHeight.wrappedValue != minHeight {
DispatchQueue.main.async {
self.$calculatedHeight.wrappedValue = self.minHeight // !! must be called asynchronously
}
}
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textViewDidChange(_ textView: UITextView) {
// This is needed for multistage text input (eg. Chinese, Japanese)
if textView.markedTextRange == nil {
parent.text = textView.text ?? String()
parent.recalculateHeight(view: textView)
}
}
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.textColor == UIColor.lightGray {
textView.text = nil
textView.textColor = UIColor.black
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
textView.text = parent.placeholder
textView.textColor = UIColor.lightGray
}
}
}
}
Use it like this:
struct ContentView: View {
#State var text: String = ""
#State var textHeight: CGFloat = 150
var body: some View {
ScrollView {
TextView(placeholder: "", text: self.$text, minHeight: self.textHeight, calculatedHeight: self.$textHeight)
.frame(minHeight: self.textHeight, maxHeight: self.textHeight)
}
}
}
Currently, the best solution is to use this package I created called TextView.
You can install it using Swift Package Manager (explained in the README). It allows for toggle-able editing state, and numerous customizations (also detailed in the README).
Here's an example:
import SwiftUI
import TextView
struct ContentView: View {
#State var input = ""
#State var isEditing = false
var body: some View {
VStack {
Button(action: {
self.isEditing.toggle()
}) {
Text("\(isEditing ? "Stop" : "Start") editing")
}
TextView(text: $input, isEditing: $isEditing)
}
}
}
In that example, you first define two #State variables. One is for the text, which the TextView writes to whenever it is typed in, and another is for the isEditing state of the TextView.
The TextView, when selected, toggles the isEditing state. When you click the button, that also toggles the isEditing state which will show the keyboard and select the TextView when true, and deselect the TextView when false.
SwiftUI has TextEditor, which is akin to TextField but offers long-form text entry which wraps into multiple lines:
var body: some View {
NavigationView{
Form{
Section{
List{
Text(question6)
TextEditor(text: $responseQuestion6).lineLimit(4)
Text(question7)
TextEditor(text: $responseQuestion7).lineLimit(4)
}
}
}
}
}
SwiftUI TextView(UIViewRepresentable) with following parameters available:
fontStyle, isEditable, backgroundColor, borderColor & border Width
TextView(text: self.$viewModel.text, fontStyle: .body, isEditable: true, backgroundColor: UIColor.white, borderColor: UIColor.lightGray, borderWidth: 1.0)
.padding()
TextView (UIViewRepresentable)
struct TextView: UIViewRepresentable {
#Binding var text: String
var fontStyle: UIFont.TextStyle
var isEditable: Bool
var backgroundColor: UIColor
var borderColor: UIColor
var borderWidth: CGFloat
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.font = UIFont.preferredFont(forTextStyle: fontStyle)
myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.backgroundColor = backgroundColor
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
}
}
Available for Xcode 12 and iOS14, it's really easy.
import SwiftUI
struct ContentView: View {
#State private var text = "Hello world"
var body: some View {
TextEditor(text: $text)
}
}
MacOS implementation
struct MultilineTextField: NSViewRepresentable {
typealias NSViewType = NSTextView
private let textView = NSTextView()
#Binding var text: String
func makeNSView(context: Context) -> NSTextView {
textView.delegate = context.coordinator
return textView
}
func updateNSView(_ nsView: NSTextView, context: Context) {
nsView.string = text
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, NSTextViewDelegate {
let parent: MultilineTextField
init(_ textView: MultilineTextField) {
parent = textView
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
self.parent.text = textView.string
}
}
}
and how to use
struct ContentView: View {
#State var someString = ""
var body: some View {
MultilineTextField(text: $someString)
}
}
You can just use TextEditor(text: $text) and then add any modifiers for things such as height.
Just want to share my UITextView solution minus the coordinator. I noticed that SwiftUI calls UITextView.intrinsicContentSize without telling it what width it should fit in. By default UITextView assumes that it has unlimited width to lay out the content so if it has only one line of text it will return the size required to fit that one line.
To fix this, we can subclass UITextView and invalidate the intrinsic size whenever the view's width changes and take the width into account when calculating the intrinsic size.
struct TextView: UIViewRepresentable {
var text: String
public init(_ text: String) {
self.text = text
}
public func makeUIView(context: Context) -> UITextView {
let textView = WrappedTextView()
textView.backgroundColor = .clear
textView.isScrollEnabled = false
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.setContentHuggingPriority(.defaultHigh, for: .vertical)
return textView
}
public func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
class WrappedTextView: UITextView {
private var lastWidth: CGFloat = 0
override func layoutSubviews() {
super.layoutSubviews()
if bounds.width != lastWidth {
lastWidth = bounds.width
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
let size = sizeThatFits(
CGSize(width: lastWidth, height: UIView.layoutFittingExpandedSize.height))
return CGSize(width: size.width.rounded(.up), height: size.height.rounded(.up))
}
}
Here's what I came up with based on Asperi's answer. This solution doesn't require to calculate and propagate size. It uses the contentSize and intrinsicContentSize inside the TextView itself:
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: UIViewRepresentableContext<TextView>) -> UITextView {
let textView = UIKitTextView()
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ textView: UITextView, context: UIViewRepresentableContext<TextView>) {
if textView.text != self.text {
textView.text = self.text
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
final private class UIKitTextView: UITextView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
// Or use e.g. `min(contentSize.height, 150)` if you want to restrict max height
CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
init(text: Binding<String>) {
self.text = text
}
func textViewDidChange(_ textView: UITextView) {
text.wrappedValue = textView.text
}
}
}
I thought I'd share my code since the other answers aren't using the Coordinator correctly:
struct UITextViewTest: View {
#State var text = "Hello, World!"
var body: some View {
VStack {
TextField("", text: $text)
MultilineTextField(text: $text)
}
}
}
struct MultilineTextField: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> UITextView {
context.coordinator.textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
// update in case the text value has changed, we assume the UIView checks if the value is different before doing any actual work.
// fortunately UITextView doesn't call its delegate when setting this property (in case of MKMapView, we would need to set our did change closures to nil to prevent infinite loop).
uiView.text = text
// since the binding passed in may have changed we need to give a new closure to the coordinator.
context.coordinator.textDidChange = { newText in
text = newText
}
}
class Coordinator: NSObject, UITextViewDelegate {
lazy var textView: UITextView = {
let textView = UITextView()
textView.font = .preferredFont(forTextStyle: .body)
textView.delegate = self
return textView
}()
var textDidChange: ((String) -> Void)?
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
textDidChange?(textView.text)
}
}
}
I use textEditor
TextEditor(text: $text)
.multilineTextAlignment(.leading)
.cornerRadius(25)
.font(Font.custom("AvenirNext-Regular", size: 20, relativeTo: .body))
//.autocapitalization(.words)
.disableAutocorrection(true)
.border(Color.gray, width: 3)
.padding([.leading, .bottom, .trailing])
// You can use the .multiLineTextAlignment modifier
TextField("Random text", text: $text)
.multiLineTextAlignment(.leading)
// This aligns the text to the left
// There are more properties beside '.leading', more can be found at the source
Related
Creating a text field in SwiftUI using UI kit.
However, the text is displayed as overflowing.
What should I add to the custom?
Originally, I want to make this red area a text field area.
struct FirstResponderTextField: UIViewRepresentable {
#Binding var text: String
let placeholder: String
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
var becameFirstResponder = false
init(text: Binding<String>) {
self ._text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: Context) -> some UIView {
let textField = UITextField()
textField.font = UIFont.systemFont(ofSize: 40, weight: .bold).rounded()
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UIViewType, context: Context) {
if !context.coordinator.becameFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.becameFirstResponder = true
}
}
}
extension UIFont {
func rounded() -> UIFont {
if let descriptor = self.fontDescriptor.withDesign(.rounded) {
let fontSize = self.pointSize
return UIFont(descriptor: descriptor, size: fontSize)
} else {
return self
}
}
}
I'm new to SwiftUI and would appreciate any comments or feedback.
I want to highlight some sentence inside the TextEditor by giving selection on the text, then I want a new custom UIMenuController but in Swift UI, I've followed some tutorial and it works in UIKit but it can't appear when I try to implement in inside swiftUI using UIRepresentable, any ideas to add that custom UIMenuController in SwiftUI?
class Coordinator: NSObject, UITextViewDelegate
{
var text: Binding<String>
var model: ScriptField_Model
init(_ text: Binding<String>, _ model: ScriptField_Model) {
self.text = text
self.model = model
}
func textViewDidChange(_ textView: UITextView) {
self.text.wrappedValue = textView.text
self.model.text = textView.text
}
func textViewDidChangeSelection(_ textView: UITextView) {
let menuItem1: UIMenuItem = UIMenuItem(title: "Menu 1", action: #selector(onMenu1(sender:)))
// Store MenuItem in array.
let myMenuItems: [UIMenuItem] = [menuItem1]
// Added MenuItem to MenuController.
UIMenuController.shared.menuItems = myMenuItems
UIMenuController.shared.hideMenu()
}
func textFieldShouldReturn(_ textView: UITextView) -> Bool {
return true
}
#objc internal func onMenu1(sender: UIMenuItem) {
print("onMenu1")
}
}
this is the part i try to add when select the sentence :
func textViewDidChangeSelection(_ textView: UITextView) {
let menuItem1: UIMenuItem = UIMenuItem(title: "Menu 1", action: #selector(onMenu1(sender:)))
// Store MenuItem in array.
let myMenuItems: [UIMenuItem] = [menuItem1]
// Added MenuItem to MenuController.
UIMenuController.shared.menuItems = myMenuItems
UIMenuController.shared.hideMenu()
}
Any Help and reference, i will appreciate it.. thankyouu
first create class like CVEDictTextView
import Foundation
import SwiftUI
class CVEDictTextView: UITextView {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
let newInstanceItem = UIMenuItem(title: "Lookup", action:#selector(lookup))
UIMenuController.shared.menuItems = [newInstanceItem]
UIMenuController.shared.hideMenu()
if(action == #selector(lookup)){
return true
}
return false
}
#objc func lookup() {
if(self.selectedRange.location != NSNotFound ){
let str = self.value(forKey: "text") as! String
var str2 = String(str.dropFirst(self.selectedRange.location))
str2 = String(str2.dropLast(str2.count - self.selectedRange.length))
print(str2)
}
}
}
and create TextView UIViewRepresentable
struct TextView: UIViewRepresentable {
#Binding var text: String
#Binding var textStyle: UIFont.TextStyle
func makeUIView(context: Context) -> UITextView {
let textView = CVEDictTextView()
textView.delegate = context.coordinator
textView.font = UIFont.preferredFont(forTextStyle: textStyle)
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
textView.isEditable = false
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
}
func makeCoordinator() -> Coordinator {
Coordinator($text)
}
class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
init(_ text: Binding<String>) {
self.text = text
}
func textViewDidChange(_ textView: UITextView) {
self.text.wrappedValue = textView.text
}
}
}
in SwitfUI view
using it
#State private var message = "hello how are you \n\t are you ok? email : hung.phuoc.tran#gmail.com"
#State private var textStyle = UIFont.TextStyle.body
TextView(text: $message, textStyle: $textStyle)
.padding(.horizontal)
I am using the UIViewRepresentable wrapper to create a bindable multiline UITextVIew since there is still no multiline support for the native SwiftUI TextField. It all works fine as long as isScrollEnabled = true is set on the UITextView.
Is there a way to get the text to wrap without the UITextView being scrollable?
import SwiftUI
struct TextfieldIssue: View {
#State var text: String = "Hello, I am some long text that will wrap if isScrollEnabled is set to true, but not if isScrollEnabled is set to false"
var body: some View {
MultilineTextField(text: $text)
}
}
struct MultilineTextField: UIViewRepresentable {
private let view = UITextView()
#Binding var text: String
func makeUIView(context: UIViewRepresentableContext<MultilineTextField>) -> UITextView {
view.isEditable = true
view.font = UIFont.systemFont(ofSize: 24)
// Text wraps fine as long as this is true
view.isScrollEnabled = true
// Text does not wrap if this is uncommented
// view.isScrollEnabled = false
view.text = text
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<MultilineTextField>) {
uiView.text = text
}
func makeCoordinator() -> TextFieldCoordinator {
let coordinator = TextFieldCoordinator(self)
self.view.delegate = coordinator
return coordinator
}
}
class TextFieldCoordinator : NSObject, UITextViewDelegate {
var parent: MultilineTextField
init(_ multilineTextField: MultilineTextField) {
self.parent = multilineTextField
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
}
struct TextfieldIssue_Previews: PreviewProvider {
static var previews: some View {
TextfieldIssue()
}
}
Just add this to makeUIView and the text will wrap.
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
Here's the full updated makeUIView function:
func makeUIView(context: UIViewRepresentableContext<MultilineTextField>) -> UITextView {
view.isEditable = true
view.font = UIFont.systemFont(ofSize: 24)
view.isScrollEnabled = false
view.view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.delegate = context.coordinator
view.text = text
return view
}
When I create a text view:
Text("Hello World")
I can't allow the user to select text when they long press.
I've looked at using a TextField but that doesn't seem to allow for turning off text editing.
I just want to be able to display a body of text and allow the user to highlight a selection using the system text selector.
Thanks!
iOS 15.0+, macOS 12.0+, Mac Catalyst 15.0+
As of Xcode 13.0 beta 2 you can use
Text("Selectable text")
.textSelection(.enabled)
Text("Non selectable text")
.textSelection(.disabled)
// applying `textSelection` to a container
// enables text selection for all `Text` views inside it
VStack {
Text("Selectable text1")
Text("Selectable text2")
// disable selection only for this `Text` view
Text("Non selectable text")
.textSelection(.disabled)
}.textSelection(.enabled)
See also the textSelection Documentation.
iOS 14 and lower
Using TextField("", text: .constant("Some text")) has two problems:
Minor: The cursor shows up when selecting
Mayor: When a user selects some text he can tap in the context menu cut, paste and other items which can change the text regardless of using .constant(...)
My solution to this problem involves subclassing UITextField and using UIViewRepresentable to bridge between UIKit and SwiftUI.
At the end I provide the full code to copy and paste into a playground in Xcode 11.3 on macOS 10.14
Subclassing the UITextField:
/// This subclass is needed since we want to customize the cursor and the context menu
class CustomUITextField: UITextField, UITextFieldDelegate {
/// (Not used for this workaround, see below for the full code) Binding from the `CustomTextField` so changes of the text can be observed by `SwiftUI`
fileprivate var _textBinding: Binding<String>!
/// If it is `true` the text field behaves normally.
/// If it is `false` the text cannot be modified only selected, copied and so on.
fileprivate var _isEditable = true {
didSet {
// set the input view so the keyboard does not show up if it is edited
self.inputView = self._isEditable ? nil : UIView()
// do not show autocorrection if it is not editable
self.autocorrectionType = self._isEditable ? .default : .no
}
}
// change the cursor to have zero size
override func caretRect(for position: UITextPosition) -> CGRect {
return self._isEditable ? super.caretRect(for: position) : .zero
}
// override this method to customize the displayed items of 'UIMenuController' (the context menu when selecting text)
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
// disable 'cut', 'delete', 'paste','_promptForReplace:'
// if it is not editable
if (!_isEditable) {
switch action {
case #selector(cut(_:)),
#selector(delete(_:)),
#selector(paste(_:)):
return false
default:
// do not show 'Replace...' which can also replace text
// Note: This selector is private and may change
if (action == Selector("_promptForReplace:")) {
return false
}
}
}
return super.canPerformAction(action, withSender: sender)
}
// === UITextFieldDelegate methods
func textFieldDidChangeSelection(_ textField: UITextField) {
// update the text of the binding
self._textBinding.wrappedValue = textField.text ?? ""
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Allow changing the text depending on `self._isEditable`
return self._isEditable
}
}
Using UIViewRepresentable to implement SelectableText
struct SelectableText: UIViewRepresentable {
private var text: String
private var selectable: Bool
init(_ text: String, selectable: Bool = true) {
self.text = text
self.selectable = selectable
}
func makeUIView(context: Context) -> CustomUITextField {
let textField = CustomUITextField(frame: .zero)
textField.delegate = textField
textField.text = self.text
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return textField
}
func updateUIView(_ uiView: CustomUITextField, context: Context) {
uiView.text = self.text
uiView._textBinding = .constant(self.text)
uiView._isEditable = false
uiView.isEnabled = self.selectable
}
func selectable(_ selectable: Bool) -> SelectableText {
return SelectableText(self.text, selectable: selectable)
}
}
The full code
In the full code below I also implemented a CustomTextField where editing can be turned off but still be selectable.
Playground view
Code
import PlaygroundSupport
import SwiftUI
/// This subclass is needed since we want to customize the cursor and the context menu
class CustomUITextField: UITextField, UITextFieldDelegate {
/// Binding from the `CustomTextField` so changes of the text can be observed by `SwiftUI`
fileprivate var _textBinding: Binding<String>!
/// If it is `true` the text field behaves normally.
/// If it is `false` the text cannot be modified only selected, copied and so on.
fileprivate var _isEditable = true {
didSet {
// set the input view so the keyboard does not show up if it is edited
self.inputView = self._isEditable ? nil : UIView()
// do not show autocorrection if it is not editable
self.autocorrectionType = self._isEditable ? .default : .no
}
}
// change the cursor to have zero size
override func caretRect(for position: UITextPosition) -> CGRect {
return self._isEditable ? super.caretRect(for: position) : .zero
}
// override this method to customize the displayed items of 'UIMenuController' (the context menu when selecting text)
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
// disable 'cut', 'delete', 'paste','_promptForReplace:'
// if it is not editable
if (!_isEditable) {
switch action {
case #selector(cut(_:)),
#selector(delete(_:)),
#selector(paste(_:)):
return false
default:
// do not show 'Replace...' which can also replace text
// Note: This selector is private and may change
if (action == Selector("_promptForReplace:")) {
return false
}
}
}
return super.canPerformAction(action, withSender: sender)
}
// === UITextFieldDelegate methods
func textFieldDidChangeSelection(_ textField: UITextField) {
// update the text of the binding
self._textBinding.wrappedValue = textField.text ?? ""
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Allow changing the text depending on `self._isEditable`
return self._isEditable
}
}
struct CustomTextField: UIViewRepresentable {
#Binding private var text: String
private var isEditable: Bool
init(text: Binding<String>, isEditable: Bool = true) {
self._text = text
self.isEditable = isEditable
}
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> CustomUITextField {
let textField = CustomUITextField(frame: .zero)
textField.delegate = textField
textField.text = self.text
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
return textField
}
func updateUIView(_ uiView: CustomUITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = self.text
uiView._textBinding = self.$text
uiView._isEditable = self.isEditable
}
func isEditable(editable: Bool) -> CustomTextField {
return CustomTextField(text: self.$text, isEditable: editable)
}
}
struct SelectableText: UIViewRepresentable {
private var text: String
private var selectable: Bool
init(_ text: String, selectable: Bool = true) {
self.text = text
self.selectable = selectable
}
func makeUIView(context: Context) -> CustomUITextField {
let textField = CustomUITextField(frame: .zero)
textField.delegate = textField
textField.text = self.text
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return textField
}
func updateUIView(_ uiView: CustomUITextField, context: Context) {
uiView.text = self.text
uiView._textBinding = .constant(self.text)
uiView._isEditable = false
uiView.isEnabled = self.selectable
}
func selectable(_ selectable: Bool) -> SelectableText {
return SelectableText(self.text, selectable: selectable)
}
}
struct TextTestView: View {
#State private var selectableText = true
var body: some View {
VStack {
// Even though the text should be constant, it is not because the user can select and e.g. 'cut' the text
TextField("", text: .constant("Test SwiftUI TextField"))
.background(Color(red: 0.5, green: 0.5, blue: 1))
// This view behaves like the `SelectableText` however the layout behaves like a `TextField`
CustomTextField(text: .constant("Test `CustomTextField`"))
.isEditable(editable: false)
.background(Color.green)
// A non selectable normal `Text`
Text("Test SwiftUI `Text`")
.background(Color.red)
// A selectable `text` where the selection ability can be changed by the button below
SelectableText("Test `SelectableText` maybe selectable")
.selectable(self.selectableText)
.background(Color.orange)
Button(action: {
self.selectableText.toggle()
}) {
Text("`SelectableText` can be selected: \(self.selectableText.description)")
}
// A selectable `text` which cannot be changed
SelectableText("Test `SelectableText` always selectable")
.background(Color.yellow)
}.padding()
}
}
let viewController = UIHostingController(rootView: TextTestView())
viewController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 200)
PlaygroundPage.current.liveView = viewController.view
A simple workaround solution I found is to just use context menus instead:
Text($someText)
.contextMenu(ContextMenu(menuItems: {
Button("Copy", action: {
UIPasteboard.general.string = someText
})
}))
I ran into a similar problem, where I wanted in essence to select the text without allowing editing. In my case, I wanted to show the UIMenuController when the text was tapped on, without allowing editing of the text or showing the cursor or keyboard. Building on the prior answers:
import SwiftUI
import UIKit
struct SelectableText: UIViewRepresentable {
var text: String
#Binding var isSelected: Bool
func makeUIView(context: Context) -> SelectableLabel {
let label = SelectableLabel()
label.textColor = .white
label.font = .systemFont(ofSize: 60, weight: .light)
label.minimumScaleFactor = 0.6
label.adjustsFontSizeToFitWidth = true
label.textAlignment = .right
label.numberOfLines = 1
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label.text = text
return label
}
func updateUIView(_ uiView: SelectableLabel, context: Context) {
uiView.text = text
if isSelected {
uiView.showMenu()
} else {
let _ = uiView.resignFirstResponder()
}
}
}
class SelectableLabel: UILabel {
override var canBecomeFirstResponder: Bool {
return true
}
override init(frame: CGRect) {
super.init(frame: .zero)
highlightedTextColor = .gray
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
switch action {
case #selector(copy(_:)), #selector(paste(_:)), #selector(delete(_:)):
return true
default:
return super.canPerformAction(action, withSender: sender)
}
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = self.stringValue
}
override func paste(_ sender: Any?) {
guard let string = UIPasteboard.general.string else { return }
NotificationCenter.default.post(name: Notification.Name.Paste, object: nil, userInfo: [Keys.PastedString: string])
}
override func delete(_ sender: Any?) {
NotificationCenter.default.post(name: Notification.Name.Delete, object: nil)
}
override func resignFirstResponder() -> Bool {
isHighlighted = false
return super.resignFirstResponder()
}
public func showMenu() {
becomeFirstResponder()
isHighlighted = true
let menu = UIMenuController.shared
menu.showMenu(from: self, rect: bounds)
}
}
I use custom paste and delete notifications to message my model object, where the paste and delete actions are processed to update the display appropriately, which works for my purposes. Bindings could also be used.
To use:
SelectableText(text: text, isSelected: self.$isSelected)
.onTapGesture {
self.isSelected.toggle()
}
.onReceive(NotificationCenter.default.publisher(for: UIMenuController.willHideMenuNotification)) { _ in
self.isSelected = false
}
Or we can use something like this when we want show "copy" tooltip for text without allowing editing.
As benefit we will have possibility to using native view "Text" which gives us the opportunity using native methods ".font()", ".foregroundColor()" and etc. Also we can use it for group of views, for exapmple cell.
Building on the prior answers.
Playground code
import PlaygroundSupport
import SwiftUI
private class SelectableUIView: UIView {
var text: String?
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
func setup() {
self.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.showMenu)))
}
#objc func showMenu(_ recognizer: UILongPressGestureRecognizer) {
becomeFirstResponder()
let menu = UIMenuController.shared
if !menu.isMenuVisible {
menu.showMenu(from: self, rect: frame)
}
}
override func copy(_ sender: Any?) {
let board = UIPasteboard.general
board.string = text
UIMenuController.shared.hideMenu()
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(UIResponderStandardEditActions.copy)
}
}
struct SelectableView: UIViewRepresentable {
var text: String
func makeUIView(context: Context) -> UIView {
let view = SelectableUIView()
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
guard let view = uiView as? SelectableUIView else {
return
}
view.text = text
}
}
struct SelectableContainer<Content: View>: View {
private let content: () -> Content
private var text: String
public init(text: String, #ViewBuilder content: #escaping () -> Content) {
self.text = text
self.content = content
}
public var body: some View {
ZStack {
content()
SelectableView(text: text)
.layoutPriority(-1)
}
}
}
struct SelectableText: View {
private var text: String
public init(_ text: String) {
self.text = text
}
public var body: some View {
ZStack {
Text(text)
SelectableView(text: text)
.layoutPriority(-1)
}
}
}
struct TextTestView: View {
#State private var text = "text"
var body: some View {
VStack {
SelectableContainer(text: text) {
VStack(alignment: .leading) {
Text("Header")
.font(.body)
Text(text)
.background(Color.orange)
}
}
.background(Color.yellow)
SelectableText(text)
.background(Color.black)
.foregroundColor(.white)
.font(.largeTitle)
}.padding()
}
}
let viewController = UIHostingController(rootView: TextTestView())
viewController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 200)
PlaygroundPage.current.liveView = viewController.view
Playground view
Playground view
So I am trying to create a custom pagination scrollView. I have been able to create that wrapper and the content inside that wrapper consists of a custom View. Inside that custom View i have got two NavigationLink buttons when pressed should take users to two different Views.
Those NavigationLink buttons are not working.
The scrollViewWrapper is inside a NavigationView. I created a test button which is just a simple Button and that seems to work. So there is something that I am not doing correctly with NavigationLink and custom UIViewControllerRepresentable.
This is where I am using the custom wrapper.
NavigationView {
UIScrollViewWrapper {
HStack(spacing: 0) {
ForEach(self.onboardingDataArray, id: \.id) { item in
OnboardingView(onboardingData: item)
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}.frame(width: geometry.size.width, height: geometry.size.height)
.background(Color.blue)
}
The onboarding view:
struct OnboardingView: View {
var onboardingData: OnboardingModel
var body: some View {
GeometryReader { geometry in
VStack(spacing: 10) {
Spacer()
Image("\(self.onboardingData.image)")
.resizable()
.frame(width: 300, height: 300)
.aspectRatio(contentMode: ContentMode.fit)
.clipShape(Circle())
.padding(20)
Text("\(self.onboardingData.titleText)")
.frame(width: geometry.size.width, height: 20, alignment: .center)
.font(.title)
Text("\(self.onboardingData.descriptionText)")
.lineLimit(nil)
.padding(.leading, 15)
.padding(.trailing, 15)
.font(.system(size: 16))
.frame(width: geometry.size.width, height: 50, alignment: .center)
.multilineTextAlignment(.center)
Spacer(minLength: 20)
if self.onboardingData.showButton ?? false {
VStack {
Button(action: {
print("Test")
}) {
Text("Test Button")
}
NavigationLink(destination: LogInView()) {
Text("Login!")
}
NavigationLink(destination: SignUpView()) {
Text("Sign Up!")
}
}
}
Spacer()
}
}
}
}
The custom ScrollView Wrapper code:
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UIScrollViewController {
let vc = UIScrollViewController()
vc.hostingController.rootView = AnyView(self.content())
return vc
}
func updateUIViewController(_ viewController: UIScrollViewController, context: Context) {
viewController.hostingController.rootView = AnyView(self.content())
}
}
class UIScrollViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.isPagingEnabled = true
return view
}()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
As the other answers have stated, there is an issue with putting the NavigationLink inside the UIViewControllerRepresentable.
I solved this by wrapping my UIViewControllerRepresentable and a NavigationLink inside a View and programmatically activating the NavigationLink from inside the UIViewControllerRepresentable.
For example:
struct MyView: View
{
#State var destination: AnyView? = nil
#State var is_active: Bool = false
var body: some View
{
ZStack
{
MyViewControllerRepresentable( self )
NavigationLink( destination: self.destination, isActive: self.$is_active )
{
EmptyView()
}
}
}
func goTo( destination: AnyView )
{
self.destination = destination
self.is_active = true
}
}
In my case, I passed the MyView instance to the UIViewController that my MyViewControllerRepresentable is wrapping, and called my goTo(destination:AnyView) method when a button was clicked.
The difference between our cases is that my UIViewController was my own class written with UIKit (compared to a UIHostingController). In the case that you're using a UIHostingController, you could probably use a shared ObservableObject containing the destination and is_active variables. You'd change your 2 NavigationLinks to Buttons having the action methods change the ObservableObject's destination and is_active variables.
This happens because you use a UIViewControllerRepresentable instead of UIViewRepresentable. I guess the UIScrollViewController keeps the destination controller from being presented by the current controller.
Try the code above instead:
import UIKit
import SwiftUI
struct ScrollViewWrapper<Content>: UIViewRepresentable where Content: View{
func updateUIView(_ uiView: UIKitScrollView, context: UIViewRepresentableContext<ScrollViewWrapper<Content>>) {
}
typealias UIViewType = UIKitScrollView
let content: () -> Content
var showsIndicators : Bool
public init(_ axes: Axis.Set = .vertical, showsIndicators: Bool = true, #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.showsIndicators = showsIndicators
}
func makeUIView(context: UIViewRepresentableContext<ScrollViewWrapper>) -> UIViewType {
let hosting = UIHostingController(rootView: AnyView(content()))
let width = UIScreen.main.bounds.width
let size = hosting.view.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
hosting.view.frame = CGRect(x: 0, y: 0, width: width, height: size.height)
let view = UIKitScrollView()
view.delegate = view
view.alwaysBounceVertical = true
view.addSubview(hosting.view)
view.contentSize = CGSize(width: width, height: size.height)
return view
}
}
class UIKitScrollView: UIScrollView, UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset) // Do whatever you want.
}
}
This is an extension to the above solution that never scrolls the inner content.
I was into a similar problem. I have figured out that the problem is with the UIViewControllerRepresentable. Instead use UIViewRepresentable, although I am not sure what the issue is. I was able to get the navigationlink work using the below code.
struct SwiftyUIScrollView<Content>: UIViewRepresentable where Content: View {
typealias UIViewType = Scroll
var content: () -> Content
var pagingEnabled: Bool = false
var hideScrollIndicators: Bool = false
#Binding var shouldUpdate: Bool
#Binding var currentIndex: Int
var onScrollIndexChanged: ((_ index: Int) -> Void)
public init(pagingEnabled: Bool,
hideScrollIndicators: Bool,
currentIndex: Binding<Int>,
shouldUpdate: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content, onScrollIndexChanged: #escaping ((_ index: Int) -> Void)) {
self.content = content
self.pagingEnabled = pagingEnabled
self._currentIndex = currentIndex
self._shouldUpdate = shouldUpdate
self.hideScrollIndicators = hideScrollIndicators
self.onScrollIndexChanged = onScrollIndexChanged
}
func makeUIView(context: UIViewRepresentableContext<SwiftyUIScrollView>) -> UIViewType {
let hosting = UIHostingController(rootView: content())
let view = Scroll(hideScrollIndicators: hideScrollIndicators, isPagingEnabled: pagingEnabled)
view.scrollDelegate = context.coordinator
view.alwaysBounceHorizontal = true
view.addSubview(hosting.view)
makefullScreen(of: hosting.view, to: view)
return view
}
class Coordinator: NSObject, ScrollViewDelegate {
func didScrollToIndex(_ index: Int) {
self.parent.onScrollIndexChanged(index)
}
var parent: SwiftyUIScrollView
init(_ parent: SwiftyUIScrollView) {
self.parent = parent
}
}
func makeCoordinator() -> SwiftyUIScrollView<Content>.Coordinator {
Coordinator(self)
}
func updateUIView(_ uiView: Scroll, context: UIViewRepresentableContext<SwiftyUIScrollView<Content>>) {
if shouldUpdate {
uiView.scrollToIndex(index: currentIndex)
}
}
func makefullScreen(of childView: UIView, to parentView: UIView) {
childView.translatesAutoresizingMaskIntoConstraints = false
childView.leftAnchor.constraint(equalTo: parentView.leftAnchor).isActive = true
childView.rightAnchor.constraint(equalTo: parentView.rightAnchor).isActive = true
childView.topAnchor.constraint(equalTo: parentView.topAnchor).isActive = true
childView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor).isActive = true
}
}
Then create a new class to handle the delegates of a scrollview. You can include the below code into the UIViewRepresentable as well. But I prefer keeping it separated for a clean code.
class Scroll: UIScrollView, UIScrollViewDelegate {
var hideScrollIndicators: Bool = false
var scrollDelegate: ScrollViewDelegate?
var tileWidth = 270
var tileMargin = 20
init(hideScrollIndicators: Bool, isPagingEnabled: Bool) {
super.init(frame: CGRect.zero)
showsVerticalScrollIndicator = !hideScrollIndicators
showsHorizontalScrollIndicator = !hideScrollIndicators
delegate = self
self.isPagingEnabled = isPagingEnabled
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let currentIndex = scrollView.contentOffset.x / CGFloat(tileWidth+tileMargin)
scrollDelegate?.didScrollToIndex(Int(currentIndex))
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let currentIndex = scrollView.contentOffset.x / CGFloat(tileWidth+tileMargin)
scrollDelegate?.didScrollToIndex(Int(currentIndex))
}
func scrollToIndex(index: Int) {
let newOffSet = CGFloat(tileWidth+tileMargin) * CGFloat(index)
contentOffset = CGPoint(x: newOffSet, y: contentOffset.y)
}
}
Now to implement the scrollView use the below code.
#State private var activePageIndex: Int = 0
#State private var shouldUpdateScroll: Bool = false
SwiftyUIScrollView(pagingEnabled: false, hideScrollIndicators: true, currentIndex: $activePageIndex, shouldUpdate: $shouldUpdateScroll, content: {
HStack(spacing: 20) {
ForEach(self.data, id: \.id) { data in
NavigationLink(destination: self.getTheNextView(data: data)) {
self.cardView(data: data)
}
}
}
.padding(.horizontal, 30.0)
}, onScrollIndexChanged: { (newIndex) in
shouldUpdateScroll = false
activePageIndex = index
// Your own required handling
})
func getTheNextView(data: Any) -> AnyView {
// Return the required destination View
}
Don't forget to add your hosting controller as a child.
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
addChild(self.hostingController)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
Did you set up the Triggered Segues? If you are using Xcode you can right click the button you created in the main storyboard. If it's not set up you can go to the connections inspector on the top right sidebar where you can find the File inspector,Identity inspector, Attributes inspector... and specify the action of what you want your button to do.