I've been trying to create a TabView with PageIndexViewStyle where each page is a ScrollView that contains an image and a WebView. The example code here from #Asperi works great when I'm creating a page outside of the TabView, but when I move things inside the TabView, the WebView on the second page isn't displayed.
import SwiftUI
import WebKit
struct Webview : UIViewRepresentable {
#Binding var dynamicHeight: CGFloat
var webview: WKWebView = WKWebView()
class Coordinator: NSObject, WKNavigationDelegate {
var parent: Webview
init(_ parent: Webview) {
self.parent = parent
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in
DispatchQueue.main.async {
self.parent.dynamicHeight = height as! CGFloat
}
})
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
webview.scrollView.bounces = false
webview.navigationDelegate = context.coordinator
let htmlStart = "<HTML><HEAD><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\"></HEAD><BODY>"
let htmlEnd = "</BODY></HTML>"
let dummy_html = """
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ut venenatis risus. Fusce eget orci quis odio lobortis hendrerit. Vivamus in sollicitudin arcu. Integer nisi eros, hendrerit eget mollis et, fringilla et libero. Duis tempor interdum velit. Curabitur</p>
<p>ullamcorper, nulla nec elementum sagittis, diam odio tempus erat, at egestas nibh dui nec purus. Suspendisse at risus nibh. Mauris lacinia rutrum sapien non faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec interdum enim et augue suscipit, vitae mollis enim maximus.</p>
<p>Fusce et convallis ligula. Ut rutrum ipsum laoreet turpis sodales, nec gravida nisi molestie. Ut convallis aliquet metus, sit amet vestibulum risus dictum mattis. Sed nec leo vel mauris pharetra ornare quis non lorem. Aliquam sed justo</p>
"""
let htmlString = "\(htmlStart)\(dummy_html)\(htmlEnd)"
webview.loadHTMLString(htmlString, baseURL: nil)
return webview
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
struct ContentView: View {
#State private var webViewHeight: CGFloat = .zero
var body: some View {
TabView {
ScrollView {
VStack {
Image(systemName: "doc")
.resizable()
.scaledToFit()
.frame(height: 100)
Divider()
Webview(dynamicHeight: $webViewHeight)
.padding(.horizontal)
.frame(height: webViewHeight)
}
}
ScrollView {
VStack {
Image(systemName: "star")
.resizable()
.scaledToFit()
.frame(height: 100)
Divider()
Webview(dynamicHeight: $webViewHeight)
.padding(.horizontal)
.frame(height: webViewHeight)
}
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
I can't exactly tell you what is causing that odd behaviour but setting the selection parameter of the TabView solved the problem.
From my understanding, SwiftUI wanted us to provide a selection binding on our TabView.
So, Changing
TabView {
to
TabView(selection: $selectedtab) {
// Or
TabView(selection: .constant(1)) {
Solved it.
Related
I need to expand each row of a List with a smooth animation (from top to bottom). A row has a main text (always displayed) and a secondary text that is shown only when row is expanded.
Here is code for List:
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach((1...3), id: \.self) { _ in
ItemRow()
}
}
.navigationTitle("Demo")
.navigationBarTitleDisplayMode(.inline)
}
}
}
And for ItemRow:
struct ItemRow: View {
#State private var expanded = false
var body: some View {
VStack {
HStack {
Text("Hello world")
Spacer()
Image(systemName: "chevron.forward.circle")
.rotationEffect(.degrees(expanded ? 90 : 0))
.onTapGesture {
withAnimation() {
expanded.toggle()
}
}
}
if expanded {
Text("Lorem ipsum dolor sit amet. Vel dicta error qui vero incidunt et fugit quisquam aut modi praesentium qui veritatis sed ipsam magnam. Ad iure velit ut possimus voluptatem cum dolores dicta. Ex cupiditate libero ut impedit internos aut reprehenderit molestias! Aut debitis dignissimos sit incidunt internos aut mollitia explicabo aut vitae numquam et repellendus iusto.")
.foregroundColor(.secondary)
.font(.caption)
.frame(height: expanded ? nil : 0)
.clipped()
}
}
}
}
Rows expand when I tap on chevron button. But the animation is not smooth at all: the main text starts in center at first and moves on top of row. Demo video
The goal is that main text stays on top (fixed) and the secondary text (and all row height) expands from top to bottom.
If I replace List in ContentView with ScrollView the animation is a bit better. But the UI does not look like a standard List anymore. And I need List features (like swipeActions).
Any idea how to make a great animation by keeping List?
The best solution is DisclosureGroup from Apple.
Just replace your ItemRow with the following:
struct ItemRow: View {
var body: some View {
DisclosureGroup {
Text("Lorem ipsum dolor sit amet. Vel dicta error qui vero incidunt et fugit quisquam aut modi praesentium qui veritatis sed ipsam magnam. Ad iure velit ut possimus voluptatem cum dolores dicta. Ex cupiditate libero ut impedit internos aut reprehenderit molestias! Aut debitis dignissimos sit incidunt internos aut mollitia explicabo aut vitae numquam et repellendus iusto.")
.foregroundColor(.secondary)
.font(.caption)
.clipped()
} label: {
Text("Hello world")
}
}
}
This is the animation you get:
Since DiscloureButtonStyle is iOS 16 Beta only, here is a trick to show custom chevron (I don't recommend this approach btw):
struct ItemRow: View {
#State private var expanded = false
var body: some View {
DisclosureGroup(isExpanded: $expanded) {
Text("Lorem ipsum dolor sit amet. Vel dicta error qui vero incidunt et fugit quisquam aut modi praesentium qui veritatis sed ipsam magnam. Ad iure velit ut possimus voluptatem cum dolores dicta. Ex cupiditate libero ut impedit internos aut reprehenderit molestias! Aut debitis dignissimos sit incidunt internos aut mollitia explicabo aut vitae numquam et repellendus iusto.")
.foregroundColor(.secondary)
.font(.caption)
.clipped()
} label: {
HStack {
Text("Hello world")
Spacer()
Image(systemName: "chevron.forward.circle")
.rotationEffect(.degrees(expanded ? 90 : 0))
}.padding(.trailing, -20) // <-- To use the space of the default one
}.accentColor(.clear) // <-- To hide the default one
}
}
I am using SwiftUI 2's new TextEditor. What I'd like to do is the TextEditor to scroll to top automatically when it's text changes which currently is not the case.
Here's the example project's code:
import SwiftUI
struct ContentView: View {
#State private var text: String = Self.text1
static private var text1 = """
Text 1:
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.
"""
static private var text2 = """
Text 2:
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.
"""
var body: some View {
VStack {
TextEditor(text: $text)
.frame(maxHeight: 150)
Button("Toggle Text") {
if text == Self.text1 {
text = Self.text2
} else {
text = Self.text1
}
}
}
}
}
I found a few threads concerning ScrollView that did not help, couldn't find anything on TextEditor though.
Here is a possible solution using ScrollViewReader for SwiftUI 2.0 and ScrollView
var body: some View {
ScrollViewReader { sp in
ScrollView {
TextEditor(text: $text)
.id(0)
}.frame(maxHeight: 150)
Button("Toggle Text") {
if text == Self.text1 {
text = Self.text2
sp.scrollTo(0, anchor: .top)
} else {
text = Self.text1
sp.scrollTo(0, anchor: .top)
}
}
}
}
I have this piece of code:
var body: some View {
ZStack {
Color.black
.opacity(0.8)
.edgesIgnoringSafeArea(.all)
(Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do ")
+
Text("eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, ")
.underline(true, color: Color.yellow)
+
Text("quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."))
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
.lineSpacing(4.0)
.padding(.horizontal, 12)
}
}
I would like to customize the underline, like increasing the offset and the line height. How can I do so?
Thank you for your help
I have a very long text and I want to show just 3 lines with more button just like the picture and also a less button when the text is expand. Any idea of how to do it with SwiftUI?
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.")
}
}
This answer is a bit of a hack, because it does not truncate the actual string and apply the "..." suffix, which in my humble opinion would be the better engineered solution. That would require the programmer to determine the length of the string that fits within three lines, remove the last two words (to allow for the More/Less button) and apply the "..." suffix.
This solution limits the number of lines shown and literally covers the trailing end of the third line with a white background and the button. But it may be suitable for your case...
#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)
}
)
}
}
You can learn how to do this by following Apple's "Introducing SwiftUI" tutorials. Specifically the "Creating a macOS app" tutorial, "Section 9 Build the Detail View".
I was also trying to achieve the similar Results as you. Also spent hours finding the solution when it was in front of me all along...!
The solution is to use ZStack along with ScrollView and GeometaryReader all at once...
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)
}
}
The Most important part of this code is CGFloat(22 * lineLimit) here 22 is the height of single Line with the specified font used. You might have to change the height (i.e. 22 in this case) based upon your font...
Other than that every thing is pretty straight forward. I hope it might help...!
You can read this Medium article
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"
}
}
}
And use this ExpandableText in your view like bellow
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)
I am trying to build a view with the newly introduced TextEditor. The idea is that I have some content at the top (blue frame), then a ScrollView with a TextEditor and a variable number of Text below it (red frame).
The TextEditor(yellow frame) view is supposed to have a minimum height, but should take up all the available space if there aren't to many Text views following – which it currently does not do...
import SwiftUI
struct ScrollViewWithTextEditor: View {
var comments = ["Foo", "Bar", "Buzz"]
var loremIpsum = """
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.
"""
var body: some View {
VStack {
Group {
Text("Some Content above")
}
.frame(maxWidth: .infinity)
.border(Color.blue, width: 3.0)
.padding(.all, 10)
ScrollView {
ScrollView {
TextEditor(text: .constant(loremIpsum))
.frame(minHeight: 200.0)
}
.frame(minHeight: 200.0)
.border(Color.yellow, width: 3.0)
.cornerRadius(3.0)
.padding(.all, 10.0)
VStack {
ForEach(comments, id: \.self) { comment in
Text(comment)
}
.padding(.all, 10)
.frame(maxWidth: .infinity, alignment: .leading)
.border(Color.gray, width: 1)
.cornerRadius(3.0)
.padding(.all, 10)
}
}
.frame(minHeight: 200.0)
.border(Color.red, width: 3)
.padding(.all, 3)
}
}
}
struct ScrollViewWithTextEditor_Previews: PreviewProvider {
static var previews: some View {
ScrollViewWithTextEditor()
}
}
Any suggestions on how to solve this?
Here is possible solution. Tested with Xcode 12 / iOS 14.
ScrollView {
// make clear static text in background to define size and
// have TextEditor in front with same text fit
Text(loremIpsum).foregroundColor(.clear).padding(8)
.frame(maxWidth: .infinity)
.overlay(
TextEditor(text: .constant(loremIpsum))
)
}
.frame(minHeight: 200.0)
.border(Color.yellow, width: 3.0)