Post

Replies

Boosts

Views

Activity

Reply to SwiftUI TabView with List not refreshing after objected deleted from Core Data
For what it's worth, it's not the List causing the crash, it's the Detail view. When you switch tabs back to the one containing the double-pane navigation view, the onscreen Detail view is going to re-render. The one that was there has a reference to what is now a fault, and it will yield a nil value for any attributes since the underlying data has been removed.The ultimate reason for the crash is the implicitly unwrapped optional in your DetailView. You can solve it at that level by using a default presentation of some kind:var body: some View { if event.managedObjectContext == nil { // object has been deleted return Text("No Selection") .navigationBarTitle("Detail") } return Text("\(event.timestamp ?? .distantPast, formatter: dateFormatter)") .navigationBarTitle("Detail") }It seems that the issue revolves around the fact that the saved view hierarchy includes a DetailView instance that contains that dangling object. I'll see if I can figure out how to force that item's removal when the List is offscreen—it's the 'offscreen' part that's throwing a wrench into the works here.
Dec ’19
Reply to Using WebKit Delegates
Your UIViewRepresentable has an associated class type called Coordinator which is used to set up these relationships. You need to implement makeCoordinator() in your WebView implementation to return an instance of your own class, and then set that class as the WKWebView's delegate.Here's a quick example:import SwiftUI import WebKit struct WebView: UIViewRepresentable { @Binding var title: String var url: URL var loadStatusChanged: ((Bool, Error?) -> Void)? = nil func makeCoordinator() -> WebView.Coordinator { Coordinator(self) } func makeUIView(context: Context) -> WKWebView { let view = WKWebView() view.navigationDelegate = context.coordinator view.load(URLRequest(url: url)) return view } func updateUIView(_ uiView: WKWebView, context: Context) { // you can access environment via context.environment here // Note that this method will be called A LOT } func onLoadStatusChanged(perform: ((Bool, Error?) -> Void)?) -> some View { var copy = self copy.loadStatusChanged = perform return copy } class Coordinator: NSObject, WKNavigationDelegate { let parent: WebView init(_ parent: WebView) { self.parent = parent } func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { parent.loadStatusChanged?(true, nil) } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { parent.title = webView.title ?? "" parent.loadStatusChanged?(false, nil) } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { parent.loadStatusChanged?(false, error) } } } struct Display: View { @State var title: String = "" @State var error: Error? = nil var body: some View { NavigationView { WebView(title: $title, url: URL(string: "https://www.apple.com/")!) .onLoadStatusChanged { loading, error in if loading { print("Loading started") self.title = "Loading…" } else { print("Done loading.") if let error = error { self.error = error if self.title.isEmpty { self.title = "Error" } } else if self.title.isEmpty { self.title = "Some Place" } } } .navigationBarTitle(title) } } } struct WebView_Previews: PreviewProvider { static var previews: some View { Display() } }
Dec ’19
Reply to How to preview a custom View that takes bindings as inputs in its initializer?
I wound up creating a wrapper view specifically to enable proper binding support for previews. The reason using an @State within the preview doesn't work is because the 'previews' property isn't considered a 'body' method by the runtime, and @State complains if you try to get bindings outside of a body method call (it calls fatalError()).Here's what I use:struct StatefulPreviewWrapper<Value, Content: View>: View { @State var value: Value var content: (Binding<Value>) -> Content var body: some View { content($value) } init(_ value: Value, content: @escaping (Binding<Value>) -> Content) { self._value = State(wrappedValue: value) self.content = content } }This takes a ViewBuilder block to which it passes a Binding ready to use. You then put it into action like so:struct ContentView: View { @Binding var enabled: Bool var body: some View { Text("Currently enabled? \(enabled ? "Yes" : "No")") Toggle("Toggle Me", isOn: $enabled) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { StatefulPreviewWrapper(false) { ContentView(enabled: $0) } } }
Dec ’19
Reply to Simple layout still fails
It looks like the Picker types are set to use the device width by default. That would match with the default behavior of UIPickerView I believe. This then causes the lower HStack to be 2 screens wide, and the upper HStack grows to match, pushing the text either side of the spacer to the far left & right, off the screen.You can force the pickers to become narrower by using the .frame() modifier to specify a minimum width of 0, thus allowing them to shrink (by default they're fixed-size):struct ContentView: View { @State var choice: Int = 10 @State var choice2: Int = 10 var body: some View { VStack { HStack { Text("Some text here") Spacer() Text("Foo baz") } HStack { Picker(selection: $choice, label: Text("C1")) { ForEach(0..<10) { n in Text("\(n) a").tag(n) } } .frame(minWidth: 0) Picker(selection: $choice2, label: Text("C2")) { ForEach(0..<10) { n in Text("\(n) b").tag(n) } } .frame(minWidth: 0) } } } }There's a slight visual artifact with this approach though, as it seems the semitransparent divider lines used to focus the selected row are still rendering full-width and are overlapping in the center of your view, making a darker line there. You can add a .hidden() modifier to one of the pickers to verify that.Happily, there's a fix for that, too: tell the pickers to clip their contents with the .clipped() modifier:Picker(selection: $choice2, label: Text("C2")) { ForEach(0..<10) { n in Text("\(n) b").tag(n) } } .frame(minWidth: 0) .clipped()All will then be well with the world. Note though that there's a space between the pickers at this point. If you want them to line up perfectly (e.g. to mimic the multi-column appearance of a date/time picker) you'd need to set a spacing of zero on the containing HStack:HStack(spacing: 0) { ... }
Dec ’19
Reply to How to embed List in ScrollView using SwiftUI
List should already scroll—it's equivalent to UITableView. Simply remove the ScrollView and your application should work. For instance this will almost match what you've described above:List { Text("abc") Text("def") } .border(Color.yellow, width: 3) .background(Color.blue) .padding(10) .border(Color.red, width: 3)Now, if your aim is to have the red border and the padding remain on screen while the list and its yellow border are scrolling, then you may have to do things a little differently. In this case, you can switch out the List with a ForEach to iterate over a collection, manually using a VStack to place a Divider view beneath each cell:ScrollView { ForEach(names, id: \.self) { name in VStack { Text(name) Divider() } } .border(Color.yellow, width: 3) .background(Color.blue) } .padding(10) .border(Color.red, width: 3)Note that only a List will provide the appearance of 'empty' cells to fill up the screen.
Dec ’19
Reply to How to align slider labels in SwiftUI
This requires the a similar approach to one I outlined in an answer to an earlier question. The current best source of information on this from inside Apple is WWDC 2019 session 237, starting around the 19:35 mark, where they talk about defining custom alignments.Now, there's evidently an alignment guide that's set by the system to refer to the leading edge of the control itself, even when displaying a label. This is how, on macOS, you get the alignment behavior you describe from items within a Form. That alignment guide value isn't public, though, so we can't take advantage of it directly. Instead, we'll have to either implement the label separately (so we can get the coordinate of the leading edge of the control) or always define the label using a ViewBuilder, so we can associate our guide with the trailing edge of the label.What you need to do is to create a new custom alignment value, and tell your VStack to align its content using that. Then, within the VStack, you set the value for this new alignment to be the leading edge of your slider or the trailing edge of your text. You set the alignment value using the .alignmentGuide() modifier on the view whose coordinates you need: this would be the slider, if you're using its leading edge, or the label to use its trailing edge. Here we'll use the trailing edge of the label, which we'll define using a ViewBuilder rather than the StringProtocol/LocalizedStringKey initializer parameter; that will let us attach the guide to the Text view we use for the label's content.The aligment itself is a custom type, but one that's very straightforward to make. You're going to create an extension on HorizontalAlignment to define a new static value there. That value will initialize a new HorizontalAlignment value using an identifier you also specify. The identifier type needs to conform to AlignmentID, which merely means it'll define a default value to use when one isn't specified manually via the .alignmentGuide() view modifier:extension HorizontalAlignment { private enum ControlLeadingEdgeAlignment: AlignmentID { static func defaultValue(in context: ViewDimensions) -> CGFloat { context[.center] // default is center, but could be anything really. } } static let controlLeadingEdge = HorizontalAlignment(ControlLeadingEdgeAlignment.self) }With this in place, you'll use your new .controlLeadingEdge alignment in two places. First, you'll supply it as the alignment argument to your VStack's initializer. Then, you'll attach a .alignmentGuide() view modifier to each of your sliders' labels, explicitly defining its value as equal to '.trailing'. Because this alignment guide doesn't exist on the HStack type, the value from your slider will bubble up through that to be seen by the VStack. Note that this doesn't work with built-in guides: you can't just redefine '.trailing' this way, as the HStack already has its own idea what that means, and will 'override' yours.This can be broken out into a View type quite neatly to always define that layout guide with minimal effort, or you can simply do everything inline. Here's an example of how you'd use it:VStack(alignment: .controlLeadingEdge) { // align subviews by aligning guides for '.controlLeadingEdge' HStack { Slider(value: $value1) { Text("Title") .font(.headline) .alignmentGuide(.controlLeadingEdge) { $0[.trailing] } // .controlLeadingEdge == label.trailing } Slider(value: $value2) { Text("Much Longer Title") .font(.headline) .alignmentGuide(.controlLeadingEdge) { $0[.trailing] } // .controlLeadingEdge == label.trailing } } }
Dec ’19
Reply to AttributeGraph: cycle detected
I'm not sure if that's a bad pattern to use, but it's the closest I could get to a guess at the sort of thing that might cause a dependency cycle. Generally, your views are a function of your data, so if the creation of a view affects some state data that's used to determine whether that view should appear, or what form it should take, that's likely how a cycle would appear.It may be something that affects @ObservedObject in particular because I believe it's publishing 'updated' notifications based on the object as a whole, not for individual properties. So, if your object has two properties, and View A uses/modifies property 1 to present View B which uses/modifies property 2, then both are going to depend on the same object, albeit in a way where SwiftUI can't easily determine if there's an order of precedence there. Where possible, try passing bindings down the stack, so the precedence is clear. It's useful to treat @ObservedObject like @State in this respect: there's a single source of truth where that lives, and everything else binds to it, or to the values within it. I don't recall off the top of my head whether '$myObservedObject' would yield a Binding to your observed object itself (i.e. with implied precedence), but '$myObservedObject.someProperty' definitely creates a binding to the underlying value. If you can use that, you'll likely find things easier to reason about.Another way to think of it is as owner vs. editor. One thing owns state, the other is a helper purely for editing the state. That's the design of the system controls, for example: none of them own any state directly, they just modify something else. View presentation state and suchlike are ultimately owned by ancillary objects created as view modifiers or (internal) generic wrapper views.In general, SwiftUI would like you to use value types wherever you can. When the project started, that was the whole ethos: using immutable value types for as much as possible, with mutations being a) locked down and minimized, and b) closely monitored to inform regeneration of immutable view descriptions. Views in particular were *only* value types, and the means of updating them was always to re-create the structure itself. That was a conscious decision to prevent bugs related to mutable view state.For a good overview, WWDC 2016 session 419 can basically be thought of as "lessons learned while writing SwiftUI." It's two engineers on that team describing the ethos behind what would eventually be named SwiftUI.
Dec ’19
Reply to SwiftUI - How to print HTML String to PDF?
SwiftUI doesn't have any special API for this. You'd likely need to look at the UIKit printing APIs: https://developer.apple.com/documentation/uikit/printingThose APIs are geared towards sending data to a printer, but the UIPrintFormatter ultimately draws to a graphics context. You might be able to use a UIMarkupPrintFormatter to write to a CGPDFContextRef created using UIGraphicsBeginPDFContextToFile() or UIGraphicsBeginContextToData(). This would then call UIGraphicsBeginPDFPage() before calling UIPrintFormatter.draw(in:forPageAt:) to render each page.Here's a rough skeleton, completely untested:let html: String = ... let outputURL: URL = ... let formatter = UIMarkupTextPrintFormatter(markupText: html) UIGraphicsBeginPDFContextToFile(outputURL.path, .zero, nil) for pageIndex in 0.. UIGraphicsBeginPDFPage() formatter.draw(in: UIGraphicsGetPDFContextBounds(), forPageAt: pageIndex) } UIGraphicsEndPDFContext()
Dec ’19
Reply to AttributeGraph: cycle detected
AttributeGraph is an internal component used by SwiftUI to build a dependency graph for your data and its related views. This is how it determines that when some State value somewhere changes that it needs to update these three views, but not these two sub-views of one of them.I'd advise looking at the state of your bindings and observed objects, and whether you can simplify how you're using them in any way. For example, you might be passing around an ObservedObject reference rather than individual single-use bindings to its content, and that might result in circular dependencies, for instance:ContentView: - @ObservedObject someThing - body: - if someThing.someValue { - show ModalView(someThing): - @ObservedObject someThing - someFunc() - modify someThing.someValue - body: - if someThing.someValue - possible circular dependency?I'm sort of reaching towards the thought that both the modal view and the view presenting that modal view are both dependending on the same piece of state somehow, with the modal view determining its content based on that state while the presenter decides whether to show the modal using the same value (or same container ObservedObject perhaps).You may be able to track this down with the aid of Instruments. Specifically, under SwiftUI there's a ‘View Properties’ instrument that looks at how dynamic view properties are changing over time, and this may provide output that will clearly indicate the presence of a dependency cycle.Thinking about it, maybe there's a static analysis tool that will find it, too.
Dec ’19
Reply to SwiftUI - Determining Current Device and Orientation
Ultimately you'll probably have to drop down to UIKit to use the UIDevice API. You can opt to receive notifications when it changes, and can use an environment object to watch these:final class OrientationInfo: ObservableObject { enum Orientation { case portrait case landscape } @Published var orientation: Orientation private var _observer: NSObjectProtocol? init() { // fairly arbitrary starting value for 'flat' orientations if UIDevice.current.orientation.isLandscape { self.orientation = .landscape } else { self.orientation = .portrait } // unowned self because we unregister before self becomes invalid _observer = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { [unowned self] note in guard let device = note.object as? UIDevice else { return } if device.orientation.isPortrait { self.orientation = .portrait } else if device.orientation.isLandscape { self.orientation = .landscape } } } deinit { if let observer = _observer { NotificationCenter.default.removeObserver(observer) } } } struct ContentView: View { @EnvironmentObject var orientationInfo: OrientationInfo var body: some View { Text("Orientation is '\(orientationInfo.orientation == .portrait ? "portrait" : "landscape")'") } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .environmentObject(OrientationInfo()) } }
Dec ’19
Reply to Binding value from an ObservableObject
ObservableObject will provide you with a binding to any contained property automatically via the $-prefix syntax:class Car: ObservableObject { @Published var isReadyForSale = true } struct SaleView: View { @Binding var isOn: Bool var body: some View { Toggle("Ready for Sale", isOn: $isOn) } } struct ContentView: View { @ObservedObject var car: Car var body: some View { Text("Details") .font(.headline) SaleView(isOn: $car.isReadyForSale) // generates a Binding to 'isReadyForSale' property } }
Dec ’19
Reply to SwiftUI How to preset a TextField
Ah, that little problem. Yeah, I've been bitten by that one. Happily, I worked out a nice little suite of Binding initializers to work around the issue in different ways.I posted an article about it with a full description of the mechanics, but there are a few different problems, each with their own solution. Frequently these build on the built-in Binding initializer that unwraps an Optional value to return a binding to a non-nil value, or a nil binding:/// Creates an instance by projecting the base optional value to its /// unwrapped value, or returns `nil` if the base value is `nil`. public init?(_ base: Binding<Value?>)For when your value might be nil, and you want to assign a non-nil 'empty' value automatically (e.g. for a non-optional property on a newly-created CoreData object), here's a Binding initializer that initializes that for you:extension Binding { init(_ source: Binding<Value?>, _ defaultValue: Value) { // Ensure a non-nil value in `source`. if source.wrappedValue == nil { source.wrappedValue = defaultValue } // Unsafe unwrap because *we* know it's non-nil now. self.init(source)! } }If you have a CoreData property that is optional, you might want to map nil to and from a given 'none' value, for instance an empty string. This will do that for you:init(_ source: Binding<Value?>, replacingNilWith nilValue: Value) { self.init( get: { source.wrappedValue ?? nilValue }, set: { newValue in if newValue == nilValue { source.wrappedValue = nil } else { source.wrappedValue = newValue } }) }Using these two is fairly straightforward. Assuming we have an @ObservedObject referring to a CoreData object with non-optional "title: String?" and optional "notes: String?" attributes, we can now do this:Form { // Items should have titles, so use a 'default value' binding TextField("Title", text: Binding($item.title, "New Item")) // Notes are entirely optional, so use a 'replace nil' binding TextField("Notes", text: Binding($item.notes, replacingNilWith: "")) }
Dec ’19