Post

Replies

Boosts

Views

Activity

Reply to Why can’t I use @Binding to manage NavigationList selection state?
Here is the non-working version with binding. Main app: @main struct ListSelectionApp: App { 		@StateObject var viewModel = ViewModel() 		var body: some Scene { 				WindowGroup { 						ContentView(selection: $viewModel.listSelection, objects: [ListObject(text: "first"), ListObject(text:"second"), ListObject(text: "third")]) 				} 		} } ViewModel: import Foundation struct ListObject: Identifiable, Hashable { 		let id = UUID() 		let text: String } class ViewModel: ObservableObject { 		@Published var listSelection: ListObject? 		{ 				didSet { 						print("did set list selection to \(String(describing: listSelection))") 				} 		} } ContentView: import SwiftUI struct ContentView: View { 		@Binding var selection: ListObject? 		var objects: [ListObject] 		var body: some View { 				NavigationView { 						List { 								ForEach(objects) { object in 										NavigationLink( 												destination: 														ListObjectView(object: object), tag: object, selection: $selection, 												label: { 														Text(object.text) 												}) 								} 						}.listStyle(SidebarListStyle()) 						.toolbar { 								ToolbarItem { 										Button("Push item") { 												selection = objects.first 										} 								} 						} 						Text("Empty view, pick something.").padding() 				}.onAppear { 						DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 								selection = objects.first 						} 				} 		} } When the timer fires or when tapping the button, didSet of the ViewModel property is called as expected, but the UI doesn’t update.
Jul ’20
Reply to macOS complaining about SceneStorage
What makes you say that it is not supported? According to API documentation - https://developer.apple.com/documentation/swiftui/scenestorage, and the SwiftUI header, it also exists on Mac. @available(iOS 14.0, OSX 10.16, tvOS 14.0, watchOS 7.0, *) @propertyWrapper public struct SceneStorage<Value> : DynamicProperty {
Jul ’20
Reply to How to render multiline text in SwiftUI List with correct height?
Update, July 2020, after WWDC 2020. We got new LazyVStack API, which means we can implement this now with scrollview instead of list. So here is a solution based on ScrollView and attributed string height measuring. I discussed this with Apple engineers during a SwiftUI who said that this is if not the best/official practice, then at least a pretty good solution. It works well for me in both macOS and iOS. (I got an error from the forum system that I have too many characters, therefore including only the iOS native code. macOS NSTextView stuff is very similar in spirit.) // //&#9;Created by Jaanus Kase on 11.05.2020. //&#9;Copyright © 2020 Jaanus Kase. All rights reserved. // import SwiftUI struct NativeTextsWithManagedHeight: View { &#9;&#9;let rows = texts(count: 1000) &#9;&#9;var body: some View { &#9;&#9;&#9;&#9;GeometryReader { geometry in &#9;&#9;&#9;&#9;&#9;&#9;ScrollViewReader { scrollViewProxy in &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;VStack { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;ScrollView { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;LazyVStack(spacing: 0) { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;ForEach(0..<self.rows.count, id: \.self) { i in &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;self.makeView(geometry, text: self.rows[i]) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;Button("Scroll to row 3") { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;print("Something") &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;withAnimation { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;scrollViewProxy.scrollTo("Hello https://example.com: 1 2 3", anchor: .center) &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}.padding() &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;&#9;&#9;} &#9;&#9;&#9;&#9;} &#9;&#9;} &#9;&#9;func makeView(_ geometry: GeometryProxy, text: String) -> some View { &#9;&#9;&#9;&#9;let attributed = attributedString(for: text) &#9;&#9;&#9;&#9;let height = attributed.height(containerWidth: geometry.size.width) &#9;&#9;&#9;&#9;return NativeTextView(string: text).frame(width: geometry.size.width, height: height).id(text) &#9;&#9;} } struct NativeTextsWithManagedHeight_Previews: PreviewProvider { &#9;&#9;static var previews: some View { &#9;&#9;&#9;&#9;NativeTextsWithManagedHeight(lazyStack: true) &#9;&#9;} } #if os(iOS) typealias NativeFont = UIFont typealias NativeColor = UIColor struct NativeTextView: UIViewRepresentable { &#9;&#9;var string: String &#9;&#9;func makeUIView(context: Context) -> UITextView { &#9;&#9;&#9;&#9;let textView = UITextView() &#9;&#9;&#9;&#9;textView.isEditable = false &#9;&#9;&#9;&#9;textView.isScrollEnabled = false &#9;&#9;&#9;&#9;textView.dataDetectorTypes = .link &#9;&#9;&#9;&#9;textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) &#9;&#9;&#9;&#9;textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) &#9;&#9;&#9;&#9;textView.textContainer.lineFragmentPadding = 0 &#9;&#9;&#9;&#9;let attributed = attributedString(for: string) &#9;&#9;&#9;&#9;textView.attributedText = attributed &#9;&#9;&#9;&#9;return textView &#9;&#9;} &#9;&#9;func updateUIView(_ textView: UITextView, context: Context) { &#9;&#9;} } #endif func attributedString(for string: String) -> NSAttributedString { &#9;&#9;let attributedString = NSMutableAttributedString(string: string) &#9;&#9;let paragraphStyle = NSMutableParagraphStyle() &#9;&#9;paragraphStyle.lineSpacing = 4 &#9;&#9;let range = NSMakeRange(0, (string as NSString).length) &#9;&#9;attributedString.addAttribute(.font, value: NativeFont.systemFont(ofSize: 24, weight: .regular), range: range) &#9;&#9;attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) &#9;&#9;return attributedString } extension NSAttributedString { &#9;&#9;func height(containerWidth: CGFloat) -> CGFloat { &#9;&#9;&#9;&#9;let rect = self.boundingRect(with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude), &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9; options: [.usesLineFragmentOrigin, .usesFontLeading], &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9; context: nil) &#9;&#9;&#9;&#9;return ceil(rect.size.height) &#9;&#9;} } func texts(count: Int) -> [String] { &#9;&#9;return (1...count).map { &#9;&#9;&#9;&#9;(1...$0).reduce("Hello https://example.com:", { $0 + " " + String($1) }) &#9;&#9;} }
Jul ’20
Reply to How to get and set SwiftUI List scroll position?
Update for iOS 14 and macOS Big Sur (i.e this year’s update of SwiftUI). If using a ScrollView instead of list, there is new API for setting the scroll position: https://developer.apple.com/documentation/swiftui/scrollviewproxy/scrollto(_:anchor:) I discussed this with Apple engineers during a SwiftUI lab. No clean API to get scroll position to implement something like infinite load. But at least we can set it cleanly now. For many cases of displaying content, using ScrollView + LazyVStack is better than using a List, and then we can also use this new API.
Jul ’20
Reply to How to render multiline text in SwiftUI List with correct height?
I agree with you about two things.SwiftUI is still obviously very raw and has many rough edges. It will not be equal to AppKit or UIKit any time soon, and this is why bridging the frameworks with these ViewRepresentable things is quite elegant.Sometimes, hacks are needed for workarounds.I am still hoping for a cleaner solution to my problem, and a clearer and deeper explanation from someone who knows more about the internals, or better docs from Apple. This shouldn’t be an edge case, but rather common. And for my own education, I really want to understand why and how it works internally and how do the different pieces interact with one another. There are multiple complex pieces interacting with each other here. UI/NSTextView is quite complex, and its layout behavior seems to differ significantly based on if it is scrollable or not. I don’t entirely understand how SwiftUI sizes its list row heights in this case. UI/NSTextView layout inside the ViewRepresentable can be done with autolayout or resizing mask, all of which have different behavior and performance characteristics. Etc. I do hope to eventually get to the bottom of this and arrive at a clean, elegant, performant solution.I’m keeping my fingers crossed that maybe they bring some updates or better docs/examples at WWDC which is not too far away. And maybe in the meantime, someone can offer a cleaner solution.In the meantime, I came up with this. It meets my requirements in the sense that it sizes the list row heights correctly in most cases. It is ugly because when e.g adding rows to a list, heights get messed up because of the async call, and the performance seems to be not great, causing more redraws/layouts than is necessary. Scrolling is not great. Nevertheless, I leave it here as an interim solution.import SwiftUI let number = 200 struct ListWithNativeTexts: View { var body: some View { List(texts(count: number), id: \.self) { text in NativeTextViewWrapper(string: text) } } } struct NativeTextViewWrapper: View { @State var textViewHeight: CGFloat = 0 var string: String var body: some View { NativeTextView(string: string, textViewHeight: $textViewHeight).frame(height: textViewHeight) } } struct ListWithNativeTexts_Previews: PreviewProvider { static var previews: some View { ListWithNativeTexts() } } func texts(count: Int) -&gt; [String] { return (1...count).map { (1...$0).reduce("Hello https://example.com:", { $0 + " " + String($1) }) } } #if os(iOS) typealias NativeFont = UIFont typealias NativeColor = UIColor typealias NativeView = UIView struct NativeTextView: UIViewRepresentable { var string: String @Binding var textViewHeight: CGFloat func makeUIView(context: Context) -&gt; 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 measureHeight(view: textView) return textView } func updateUIView(_ textView: UITextView, context: Context) { measureHeight(view: textView) } } #else typealias NativeFont = NSFont typealias NativeColor = NSColor typealias NativeView = NSView struct NativeTextView: NSViewRepresentable { var string: String @Binding var textViewHeight: CGFloat func makeNSView(context: Context) -&gt; 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 measureHeight(view: textView) return textView } func updateNSView(_ textView: NSTextView, context: Context) { measureHeight(view: textView) } } #endif extension NativeTextView { func measureHeight(view: NativeView) { DispatchQueue.main.async { let bounds = view.bounds #if os(iOS) let sizeThatFits = view.sizeThatFits(CGSize(width: bounds.size.width, height: CGFloat.greatestFiniteMagnitude)) #else // https://stackoverflow.com/questions/2654580/how-to-resize-nstextview-according-to-its-content var sizeThatFits = CGSize(width: 0, height: 0) if let textView = view as? NSTextView, let layoutManager = textView.layoutManager, let textContainer = textView.textContainer { layoutManager.ensureLayout(for: textContainer) sizeThatFits = layoutManager.usedRect(for: textContainer).size } #endif self.textViewHeight = sizeThatFits.height } } } func attributedString(for string: String) -&gt; 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 }
May ’20
Reply to How to render multiline text in SwiftUI List with correct height?
This feels like a hack and against the spirit of the framework. I’m hoping there is a cleaner solution that I have missed, that doesn’t involve leaking anything out of the ViewRepresentable. I also can’t quite figure out what would be a good moment to capture the frame/height info in my solution - should I subclass UITextView/NSTextView and intercept the frame/bounds in some of the view layout methods? Uhh.
May ’20
Reply to Is it possible to have a SwiftUI button load an AR Quick Look preview?
I’m trying to implement a QuickLook preview of an image with SwiftUI. Thank you for posting this example. Almost got it working. In my case, it is a regular QLPreview of an image, not AR. So I could simplify this code and make it work.Here’s a problem I have: whether I push the preview into navigation stack with NavigationLink or show it with the sheet technique, it only shows the image itself (fully interactive and zoomable, which is great), but it does not show any of the surrounding controls (title, share button) etc. It is just a blank interface with the image. Am I doing anything wrong / is there anything missing in your example to also get the title and Share button for a regular QLPreview?
Jan ’20