"Is it correct that Developer B does not have access to the container but Developer A still does?" correct. Developer A can access the container in CloudKit dashboard. Developer B can not.For the second one - sorry, what I wrote is ambiguous. I mean: "The app signed by developer account B can still access the CK container owned by developer account A (ownership is manifested by the fact that A can access it in CK dashboard, and B can not)."
Post
Replies
Boosts
Views
Activity
Fair enough. Thank you. Looks like I should migrate all the data to a new container owned by B, and then release a new version of the app talking to that container. The volume of the data is fairly small, and it's doable by hand.I was really expecting the CloudKit container to be migrated over along with the rest of the app transfer. I can sort of understand why it wasn't (there isn’t really 1:1 mapping or association between apps and containers). I don’t believe I’m the first in the world to encounter this scenario - surely, apps using CloudKit must have been sold and transferred. Really curious what they have done in this case - especially if they use private containers, which the developer cannot migrate just like that. (My case only involves public container and all data is available to me.)
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?
Haven’t heard anything about it anywhere, sadly.
This doesn’t make a difference because .byWordWrapping is the default value for lineBreakMode. The text views wrap lines correctly if given enough space. If I set an explicit height on the row, I see everything is wrapped correctly on word breaks:NativeTextView(string: text).frame(height: 128)
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.
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) -> [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) -> 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) -> 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) -> 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
}
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.
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.)
//
//	Created by Jaanus Kase on 11.05.2020.
//	Copyright © 2020 Jaanus Kase. All rights reserved.
//
import SwiftUI
struct NativeTextsWithManagedHeight: View {
		let rows = texts(count: 1000)
		var body: some View {
				GeometryReader { geometry in
						ScrollViewReader { scrollViewProxy in
								VStack {
										ScrollView {
														LazyVStack(spacing: 0) {
																ForEach(0..<self.rows.count, id: \.self) { i in
																		self.makeView(geometry, text: self.rows[i])
																}
														}
										}
										Button("Scroll to row 3") {
												print("Something")
												withAnimation {
														scrollViewProxy.scrollTo("Hello https://example.com: 1 2 3", anchor: .center)
												}
										}.padding()
								}
						}
				}
		}
		func makeView(_ geometry: GeometryProxy, text: String) -> some View {
				let attributed = attributedString(for: text)
				let height = attributed.height(containerWidth: geometry.size.width)
				return NativeTextView(string: text).frame(width: geometry.size.width, height: height).id(text)
		}
}
struct NativeTextsWithManagedHeight_Previews: PreviewProvider {
		static var previews: some View {
				NativeTextsWithManagedHeight(lazyStack: true)
		}
}
#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) {
		}
}
#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(.paragraphStyle, value: paragraphStyle, range: range)
		return attributedString
}
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 texts(count: Int) -> [String] {
		return (1...count).map {
				(1...$0).reduce("Hello https://example.com:", { $0 + " " + String($1) })
		}
}
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 {
Update after WWDC 2020. We got LazyVStack there, which is exactly what’s needed. I discussed this with Apple engineers in a lab, who confirmed that LazyVStack + scrollview + UI/NSTextView is quite acceptable for the time being.
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.
Interesting. I tried to run exactly this, with the viewModel being an ObservedObject that gets passed in. When tapping on the list items, the UI works but appears glitchy, there are some weird flashing artifacts. Updating the selection with timer or external button press does not work.
There is only one CloudKit container per app. (Well, I guess there can be several, but mostly you work with one.) That container, though, contains both private, public, and shared databases. It is possible to mix and match the different database types. I am currently working on an app that uses all three database types. So yes, it’s definitely possible.
I am working on a cross platform app and using the app lifecycle across all platforms would be great. Since I can’t do that, my current plan is to keep using the new lifecycle on macOS, but fall back to iOS 13 way on iOS until there is a way to accept CloudKit shares in the new lifecycle.