Как отобразить многострочный текст в списке SwiftUI с правильной высотой?
Я хотел бы иметь представление SwiftUI, отображающее много строк текста, со следующими требованиями:
- Работает как на macOS, так и на iOS.
- Показывает большое количество строк (каждая строка поддерживается отдельным объектом модели).
- Я могу применить произвольный стиль к многострочному тексту.
- Каждая строка текста может иметь произвольную длину, возможно, занимать несколько строк и абзацев.
- Максимальная ширина каждой строки текста привязана к ширине контейнера. Высота варьируется в зависимости от фактической длины текста.
- Прокрутки по каждому отдельному тексту нет, только список.
- Ссылки в тексте должны быть активными / интерактивными.
- Текст доступен только для чтения, его не нужно редактировать.
Похоже, что наиболее подходящим решением было бы иметь представление списка, обертывающее собственный UITextView/NSTextView.
Вот что у меня есть на данный момент. Он реализует большинство требований, ЗА ИСКЛЮЧЕНИЕМ правильной высоты строк.
//
// ListWithNativeTexts.swift
// SUIToy
//
// Created by Jaanus Kase on 03.05.2020.
// Copyright © 2020 Jaanus Kase. All rights reserved.
//
import SwiftUI
let number = 20
struct ListWithNativeTexts: View {
var body: some View {
List(texts(count: number), id: \.self) { text in
NativeTextView(string: text)
}
}
}
struct ListWithNativeTexts_Previews: PreviewProvider {
static var previews: some View {
ListWithNativeTexts()
}
}
func texts(count: Int) -> [String] {
return (1...count).map {
(1...$0).reduce("Hello https://example.com:", { $0 + " " + String($1) })
}
}
#if os(iOS)
typealias NativeFont = UIFont
typealias NativeColor = UIColor
struct NativeTextView: UIViewRepresentable {
var string: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.isEditable = false
textView.isScrollEnabled = false
textView.dataDetectorTypes = .link
textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.textContainer.lineFragmentPadding = 0
let attributed = attributedString(for: string)
textView.attributedText = attributed
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
}
}
#else
typealias NativeFont = NSFont
typealias NativeColor = NSColor
struct NativeTextView: NSViewRepresentable {
var string: String
func makeNSView(context: Context) -> NSTextView {
let textView = NSTextView()
textView.isEditable = false
textView.isAutomaticLinkDetectionEnabled = true
textView.isAutomaticDataDetectionEnabled = true
textView.textContainer?.lineFragmentPadding = 0
textView.backgroundColor = NSColor.clear
textView.textStorage?.append(attributedString(for: string))
textView.isEditable = true
textView.checkTextInDocument(nil) // make links clickable
textView.isEditable = false
return textView
}
func updateNSView(_ textView: NSTextView, context: Context) {
}
}
#endif
func attributedString(for string: String) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: string)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 4
let range = NSMakeRange(0, (string as NSString).length)
attributedString.addAttribute(.font, value: NativeFont.systemFont(ofSize: 24, weight: .regular), range: range)
attributedString.addAttribute(.foregroundColor, value: NativeColor.red, range: range)
attributedString.addAttribute(.backgroundColor, value: NativeColor.yellow, range: range)
attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
return attributedString
}
Вот что он выводит на iOS. Вывод в macOS аналогичен.
Как мне получить это решение для определения размера текстовых представлений с правильной высотой?
Один из подходов, который я пробовал, но не показанный здесь, - это указать высоту "снаружи внутрь" - указать высоту в самой строке списка с помощью frame. Я могу вычислить высоту NSAttributedString, зная ширину, которую я могу получить с помощью geoReader. Это почти работает, но с ошибками и кажется неправильным, поэтому я не показываю это здесь.
3 ответа
Строки списка размеров не работают со SwiftUI.
Однако я разработал, как отображать прокрутку собственных UITextViews в стеке, где каждый элемент имеет динамический размер в зависимости от высоты его attributedText.
Я поставил интервал в 2 точки между каждым элементом и протестировал 80 элементов с помощью вашего текстового генератора.
Вот первые три скриншота прокрутки и еще один снимок экрана, показывающий самый конец прокрутки.
Вот полный класс с расширениями для высоты attributedText и обычного размера строки.
import SwiftUI
let number = 80
struct ListWithNativeTexts: View {
let rows = texts(count:number)
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(spacing: 2) {
ForEach(0..<self.rows.count, id: \.self) { i in
self.makeView(geometry, text: self.rows[i])
}
}
}
}
}
func makeView(_ geometry: GeometryProxy, text: String) -> some View {
print(geometry.size.width, geometry.size.height)
// for a regular string size (not attributed text)
// let textSize = text.size(width: geometry.size.width, font: UIFont.systemFont(ofSize: 17.0, weight: .regular), padding: UIEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0))
// print("textSize: \(textSize)")
// return NativeTextView(string: text).frame(width: geometry.size.width, height: textSize.height)
let attributed = attributedString(for: text)
let height = attributed.height(containerWidth: geometry.size.width)
print("height: \(height)")
return NativeTextView(string: text).frame(width: geometry.size.width, height: height)
}
}
struct ListWithNativeTexts_Previews: PreviewProvider {
static var previews: some View {
ListWithNativeTexts()
}
}
func texts(count: Int) -> [String] {
return (1...count).map {
(1...$0).reduce("Hello https://example.com:", { $0 + " " + String($1) })
}
}
#if os(iOS)
typealias NativeFont = UIFont
typealias NativeColor = UIColor
struct NativeTextView: UIViewRepresentable {
var string: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.isEditable = false
textView.isScrollEnabled = false
textView.dataDetectorTypes = .link
textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.textContainer.lineFragmentPadding = 0
let attributed = attributedString(for: string)
textView.attributedText = attributed
// for a regular string size (not attributed text)
// textView.font = UIFont.systemFont(ofSize: 17.0, weight: .regular)
// textView.text = string
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
}
}
#else
typealias NativeFont = NSFont
typealias NativeColor = NSColor
struct NativeTextView: NSViewRepresentable {
var string: String
func makeNSView(context: Context) -> NSTextView {
let textView = NSTextView()
textView.isEditable = false
textView.isAutomaticLinkDetectionEnabled = true
textView.isAutomaticDataDetectionEnabled = true
textView.textContainer?.lineFragmentPadding = 0
textView.backgroundColor = NSColor.clear
textView.textStorage?.append(attributedString(for: string))
textView.isEditable = true
textView.checkTextInDocument(nil) // make links clickable
textView.isEditable = false
return textView
}
func updateNSView(_ textView: NSTextView, context: Context) {
}
}
#endif
func attributedString(for string: String) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: string)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 4
let range = NSMakeRange(0, (string as NSString).length)
attributedString.addAttribute(.font, value: NativeFont.systemFont(ofSize: 24, weight: .regular), range: range)
attributedString.addAttribute(.foregroundColor, value: NativeColor.red, range: range)
attributedString.addAttribute(.backgroundColor, value: NativeColor.yellow, range: range)
attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
return attributedString
}
extension String {
func size(width:CGFloat = 220.0, font: UIFont = UIFont.systemFont(ofSize: 17.0, weight: .regular), padding: UIEdgeInsets? = nil) -> CGSize {
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = self
label.sizeToFit()
if let pad = padding{
// add padding
return CGSize(width: label.frame.width + pad.left + pad.right, height: label.frame.height + pad.top + pad.bottom)
} else {
return CGSize(width: label.frame.width, height: label.frame.height)
}
}
}
extension NSAttributedString {
func height(containerWidth: CGFloat) -> CGFloat {
let rect = self.boundingRect(with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
return ceil(rect.size.height)
}
func width(containerHeight: CGFloat) -> CGFloat {
let rect = self.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: containerHeight),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
return ceil(rect.size.width)
}
}
Иеан,
Я не уверен, что полностью понимаю ваш вопрос, но есть несколько переменных среды и вставок, которые вы можете добавить, чтобы изменить интервал между представлениями списка SwiftUI... Вот пример того, о чем я говорю.
Обратите внимание, что важно добавить их в правильное представление, listRowInsets находится в ForEach, среда находится в представлении List.
List {
ForEach((0 ..< self.selections.count), id: \.self) { column in
HStack(spacing:0) {
Spacer()
Text(self.selections[column].name)
.font(Fonts.avenirNextCondensedBold(size: 22))
Spacer()
}
}.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}.environment(\.defaultMinListRowHeight, 20)
.environment(\.defaultMinListHeaderHeight, 0)
.frame(width: UIScreen.main.bounds.size.width, height: 180.5, alignment: .center)
.offset(x: 0, y: -64)
отметка
"Я хотел бы иметь представление SwiftUI, которое показывает много строк текста,..."
Только с использованием SwiftUI:
Я считаю, что это касается всех ваших элементов в отношении SwiftUI. MacOS, я позволю вам адаптироваться. Ваш текст может быть установлен программно, как и все различные стили, которые вы выберете. Я максимально просто показал пример, используя статические свойства. Вы можете сделать большинство ячеек доступными с помощью NavigationLink
Вот предварительный просмотр в Xcode для IOS:
Вот сам код, других частей не потребовалось.
//
// ListWithNativeTextz.swift
// CRecipes
//
// Created by Zahirudeen Premji on 5/7/20.
// With credit to Paul Hudson post
import SwiftUI
import SafariServices
struct ListWithNativeTextz: View {
let number = 20
let introText = "Hello "
let urlText = "https://hackingwithswift.com: "
var body: some View {
List(texts(count: number), id: \.self) { text in
NavigationLink(destination: NewDetail()) {
Text(self.introText)
.font(.headline)
.foregroundColor(.blue)
+ Text(self.urlText)
.font(.subheadline)
.foregroundColor(.green)
+ Text(text)
.font(.caption)
.foregroundColor(.red)
}
}
}
func texts(count: Int) -> [String] {
return (1...count).map {
(1...$0).reduce("", {$0 + " " + String($1)})
}
}
}
struct NewDetail: View {
// add a SafariView here to display the link in Safari
// You must import SafariServices and then follow instruction here :
// https://stackru.com/questions/56518029/how-do-i-use-sfsafariviewcontroller-with-swiftui
// https://www.hackingwithswift.com/read/32/3/how-to-use-sfsafariviewcontroller-to-browse-a-web-page
var body: some View {
Text("Hello There")
}
}
struct ListWithNativeTextz_Previews: PreviewProvider {
static var newDetail = NewDetail()
static var previews: some View {
ListWithNativeTextz()
}
}
Включите Пола Хадсона в текст по адресу: https://www.hackingwithswift.com/quick-start/swiftui/how-to-combine-text-views-together