Как реализовать кнопку стиля "читать дальше" в конце текста в SwiftUI

У меня очень длинный текст, и я хочу показать всего 3 строки с дополнительной кнопкой, как на картинке, а также с кнопкой меньше, когда текст расширен. Есть идеи, как это сделать с помощью swiftUI[1]:

var body: some View{
    VStack{
        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
    }
}

3 ответа

Вы можете прочитать эту статью на Medium

      struct ExpandableText: View {
    
    @State private var expanded: Bool = false
    @State private var truncated: Bool = false
    @State private var shrinkText: String
    
    private var text: String
    let font: UIFont
    let lineLimit: Int
    
    init(_ text: String, lineLimit: Int, font: UIFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)) {
        self.text = text
        _shrinkText =  State(wrappedValue: text)
        self.lineLimit = lineLimit
        self.font = font
    }
    
    var body: some View {
        ZStack(alignment: .bottomLeading) {
            Group {
                Text(self.expanded ? text : shrinkText) + Text(moreLessText)
                    .bold()
                    .foregroundColor(.black)
            }
            .animation(.easeInOut(duration: 1.0), value: false)
            .lineLimit(expanded ? nil : lineLimit)
            .background(
                // Render the limited text and measure its size
                Text(text)
                    .lineLimit(lineLimit)
                    .background(GeometryReader { visibleTextGeometry in
                        Color.clear.onAppear() {
                            let size = CGSize(width: visibleTextGeometry.size.width, height: .greatestFiniteMagnitude)
                            let attributes:[NSAttributedString.Key:Any] = [NSAttributedString.Key.font: font]

                            ///Binary search until mid == low && mid == high
                            var low  = 0
                            var heigh = shrinkText.count
                            var mid = heigh ///start from top so that if text contain we does not need to loop
                            ///
                            while ((heigh - low) > 1) {
                                let attributedText = NSAttributedString(string: shrinkText + moreLessText, attributes: attributes)
                                let boundingRect = attributedText.boundingRect(with: size, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
                                if boundingRect.size.height > visibleTextGeometry.size.height {
                                    truncated = true
                                    heigh = mid
                                    mid = (heigh + low)/2
                                    
                                } else {
                                    if mid == text.count {
                                        break
                                    } else {
                                        low = mid
                                        mid = (low + heigh)/2
                                    }
                                }
                                shrinkText = String(text.prefix(mid))
                            }
                            
                            if truncated {
                                shrinkText = String(shrinkText.prefix(shrinkText.count - 2))  //-2 extra as highlighted text is bold
                            }
                        }
                    })
                    .hidden() // Hide the background
            )
            .font(Font(font)) ///set default font
            ///
            if truncated {
                Button(action: {
                    expanded.toggle()
                }, label: {
                    HStack { //taking tap on only last line, As it is not possible to get 'see more' location
                        Spacer()
                        Text("")
                    }.opacity(0)
                })
            }
        }
    }
    
    private var moreLessText: String {
        if !truncated {
            return ""
        } else {
            return self.expanded ? " read less" : " ... read more"
        }
    }
    
}

И используйте этот ExpandableText в своем представлении, как показано ниже.

      ExpandableText("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut laborum", lineLimit: 6)

Этот ответ - своего рода взлом, потому что он не усекает фактическую строку и не применяет суффикс "...", что, по моему скромному мнению, было бы лучшим спроектированным решением. Для этого программисту потребуется определить длину строки, которая умещается в трех строках, удалить последние два слова (чтобы можно было использовать кнопку "Больше / Меньше") и применить суффикс "...".

Это решение ограничивает количество показываемых строк и буквально закрывает конец третьей строки белым фоном и кнопкой. Но может подойти для вашего случая...

@State private var isExpanded: Bool = false

var body: some View{
    
    VStack{

        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
            .lineLimit(isExpanded ? nil : 3)
            .overlay(
                GeometryReader { proxy in
                    Button(action: {
                        isExpanded.toggle()
                    }) {
                        Text(isExpanded ? "Less" : "More")
                            .font(.caption).bold()
                            .padding(.leading, 8.0)
                            .padding(.top, 4.0)
                            .background(Color.white)
                    }
                    .frame(width: proxy.size.width, height: proxy.size.height, alignment: .bottomTrailing)
                }
            )
    }
}

Вы можете узнать, как это сделать, следуя руководствам Apple " Знакомство с SwiftUI". В частности, учебник "Создание приложения для macOS", " Раздел 9" Создание подробного представления"".

Я также пытался достичь таких же результатов, как и вы. Также потратил часы на поиск решения, когда оно все время было передо мной...!

Решение состоит в том, чтобы использовать ZStack вместе с ScrollView и GeometaryReader одновременно...

      struct CollapsableTextView: View {
    let lineLimit: Int
    
    @State private var expanded: Bool = false
    @State private var showViewButton: Bool = false
    private var text: String
    
    init(_ text: String, lineLimit: Int) {
        self.text = text
        self.lineLimit = lineLimit
        
    }
    
    private var moreLessText: String {
        if showViewButton {
            return expanded ? "View Less" : "View More"
            
        } else {
            return ""
            
        }
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            ZStack {
                Text(text)
                    .font(.body)
                    .lineLimit(expanded ? nil : lineLimit)
                
                ScrollView(.vertical) {
                    Text(text)
                        .font(.body)
                        .background(
                            GeometryReader { proxy in
                                Color.clear
                                    .onAppear {
                                        showViewButton = proxy.size.height > CGFloat(22 * lineLimit)
                                    }
                                    .onChange(of: text) { _ in
                                        showViewButton = proxy.size.height > CGFloat(22 * lineLimit)
                                    }
                            }
                        )
                    
                }
                .opacity(0.0)
                .disabled(true)
                .frame(height: 0.0)
            }
            
            Button(action: {
                withAnimation {
                    expanded.toggle()
                }
            }, label: {
                Text(moreLessText)
                    .font(.body)
                    .foregroundColor(.orange)
            })
            .opacity(showViewButton ? 1.0 : 0.0)
            .disabled(!showViewButton)
            .frame(height: showViewButton ? nil : 0.0)
            
        }
    }
}

struct CollapsableTextView_Previews: PreviewProvider {
    static var previews: some View {
        CollapsableTextView("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", lineLimit: 3)
    }
    
}

Самая важная часть этого кодаCGFloat(22 * lineLimit)здесь 22 — высота одной строки с указанным шрифтом. Возможно, вам придется изменить высоту (в данном случае 22) в зависимости от вашего шрифта...

В остальном все довольно прямолинейно. Я надеюсь, что это может помочь...!

Другие вопросы по тегам