SwiftUI: View flashes briefly white [duplicate] - swift

This question already has answers here:
How to prevent initial white flash when showing a UIWebView?
(4 answers)
Closed 2 years ago.
I'm a beginner SwiftUI programmer and I'm encountering the following problem:
I have two Views, one loading view (Rectangle) and one web view (WKWebView).
Using those two views like this:
var body : some View {
ZStack {
WebView(self.webView)
Rectangle()
.foregroundColor(self.state.isLoading ? self.loadingColor : Color.clear)
}
.onAppear {
// Initialize web view etc.
// Sets self.state.isLoading to false after some time has passed
}
}
Interestingly when self.state.isLoading changes from true to false the UI briefly flashes white.
I don't know why this is happening, especially because the WebView definitely isn't white at the point of showing.
I tried adding a background rectangle and setting the backgroundColor of self.webView and self.webView.scrollView:
var body : some View {
ZStack {
Rectangle()
.foregroundColor(Color.red)
WebView(self.webView)
Rectangle()
.foregroundColor(self.state.isLoading ? self.loadingColor : Color.clear)
}
.onAppear {
// Initialize web view etc.
self.webView.backgroundColor = UIColor.red
self.webView.scrollView.backgroundColor = UIColor.red
// Sets self.state.isLoading to false after some time has passed
}
}
I expected to see a brief red screen, but it remains white.
Adding an animation to the Rectangle() transition makes the flashing go away
Rectangle()
.foregroundColor(self.state.isLoading ? self.loadingColor : Color.clear)
.animation(.easeIn)
Am I missing something or doing something wrong?

I'm thinking this may have something to do with the self.state.isLoading variable and when it changes, the whole view is reloading. Can you try instead making the isLoading a local variable in the struct (#State var isLoading: Bool = false) and then toggling the local variable instead?

Related

SwiftUI - Position an overlay relative to its anchor

I have a ZStack containing 2 views:
referenceContent - has some content and a Divider. This is the main content across the screen
popoverContent - is a conditional popup window that only takes up a tiny portion of the screen.
var body: some View {
ZStack {
referenceContent
if popoverCondition {
popoverContent
}
}
}
I want the popoverContent's top edge to line up with the bottom of referenceContent
Anyone know how to make this happen? Or is there just a much better way to view this popup window than I'm doing now? Thanks!
You can do this using the overlay(alignment:content:) modifier (previously overlay(_:alignment:)) in combination with custom alignment guides.
The basic idea is that you align the bottom of your reference view with the top of your popover view.
The annoying thing is that the overlay modifier only lets you specify one alignment guide (for the two views). So if you write stack1.overlay(alignment: .bottom) { stack2 } it will align the bottom of your reference with the bottom of your overlay. A quick way to overcome this is to overwrite the bottom alignment guide of your overlay and return the top instead.
referenceView
.overlay(alignment: .bottom) {
popoverContent
// overwrites bottom alignment of the popover with its top alignment guide.
.alignmentGuide(.bottom) {$0[.top]}
}
Overlay vs ZStack
You might ask: "why don't you use a ZStack instead of an overlay?". Well the difference between the two is that the ZStack will take the size of your popover into consideration when laying out your entire view (reference + popover). That is the opposite of what a popover should do. For a popover, the layout system should only take the size of your reference view into consideration and draw the popover on top of it (without affecting the layout of your reference). That is exactly what the overlay(...) modifier does.
Old API (prior to iOS 15, macOS 12)
In older versions of SwiftUI the arguments of the overlay modifier were in reverse order. So the code example for these older systems is:
referenceView
.overlay(
popoverContent.alignmentGuide(.bottom) {$0[.top]},
alignment: .bottom
)
Custom alignment guides
When you don't want to overwrite an existing alignment guide (because you need it somewhere else for example) you can also use a custom alignment guide. Here is a more generic example using a custom alignment guide named Alignment.TwoSided
extension View {
#available(iOS 15.0, *)
func overlay<Target: View>(align originAlignment: Alignment, to targetAlignment: Alignment, of target: Target) -> some View {
let hGuide = HorizontalAlignment(Alignment.TwoSided.self)
let vGuide = VerticalAlignment(Alignment.TwoSided.self)
return alignmentGuide(hGuide) {$0[originAlignment.horizontal]}
.alignmentGuide(vGuide) {$0[originAlignment.vertical]}
.overlay(alignment: Alignment(horizontal: hGuide, vertical: vGuide)) {
target
.alignmentGuide(hGuide) {$0[targetAlignment.horizontal]}
.alignmentGuide(vGuide) {$0[targetAlignment.vertical]}
}
}
}
extension Alignment {
enum TwoSided: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat { 0 }
}
}
You would use that like this:
referenceView
.overlay(align: .bottom, to: .top, of: popoverContent)

How to disable opacity with transitions in SwiftUI?

I'm using SwiftUI.
I have a transition that I'm using to bring up my SignInView. However, this transition seems to be automatically applying an opacity effect on the view it's replacing. This wouldn't be a problem, however, it seems like the Safe Area on both the top and the bottom have different rates of receiving the opacity than the rest of the view.
I'm trying to find 1 of 2 solutions:
How can I get rid of the opacity effect altogether, or
How can I get the opacity effect to be applied evenly everywhere.
Here is the code for my transition:
struct AuthView: View {
#State var showSignIn: Bool = false
var body: some View {
ZStack {
if !showSignIn {
WelcomeView(showSignIn: $showSignIn)
} else {
SignInView(showSignIn: $showSignIn)
.transition(AnyTransition.move(edge: .trailing))
.zIndex(1)
}
}
}
}
Here is the button controlling the state variable:
Button(action: { withAnimation(.easeInOut) { showSignIn.toggle() } }) {
//Button text
}
I also have a video (GIF) that better shows what I'm talking about, when I mention the uneven distribution of opacity.
https://i.stack.imgur.com/KXEcF.gif
If you look closely at the top and bottom safe area it fades faster than the rest of the view, which is undesired.
Note: When I changed the appearance to Dark Mode, it turned black instead of white on the top and bottom.
I assume you want identity transition for original view (opacity transition is applied by default if none other specified)
if !showSignIn {
WelcomeView(showSignIn: $showSignIn)
.transition(.identity) // << here !!

Set segment equal width for SwiftUI Picker with SegmentedPickerStyle

Using the SegmentedPickerStyle style Picker could make the control looks like UISegmentedControl. But I wonder how to adjust the segment width in the picker. For examle, the picker in the image has a different width for text.
Is there a way to make the segments the same width in the SwiftUI?
Picker(selection: $store.utility.saliencyType, label: EmptyView()) {
ForEach(Store.Utility.SaliencyType.allCases, id: \.self) { saliencyType in
Text(saliencyType.text)
.tag(saliencyType)
}
}.pickerStyle(SegmentedPickerStyle())
...For examle, the picker in the image has a different width for text.
In case you arrive here seeking for iOS SwiftUI SegmentedPickerStyle solution... I've found the iOS SwiftUI .pickerStyle(SegmentedPickerStyle()) will conform to global UISegmentedControl.appearance() settings, so I've used the following to successfully apportion the width of each segment:
UISegmentedControl.appearance().apportionsSegmentWidthsByContent = true
This is particularly useful if, for example, you want to support Dynamic Type fonts in your app, which can otherwise cause segments with longer names to blow out and get truncated. [aside: I also use this trick to change the SwiftUI segmented picker's font size! see https://stackoverflow.com/a/71834578/3936065]
This is default macOS NSSegmetedControl behavirour
#property NSSegmentDistribution segmentDistribution API_AVAILABLE(macos(10.13));
// Defaults to NSSegmentDistributionFill on 10.13, older systems will continue to behave similarly to NSSegmentDistributionFit
Update: here is workaround, based on finding NSSegmentedControl in run-time view hierarchy.
Disclaimer: Actually it is safe, ie. no crash in run-time, but can stop working in future returning to default behaviour.
So, the idea is to inject NSView via representable into view hierarchy above (!!) Picker, as
Picker(selection: $store.utility.saliencyType, label: EmptyView()) {
ForEach(Store.Utility.SaliencyType.allCases, id: \.self) { saliencyType in
Text(saliencyType.text)
.tag(saliencyType)
}
}
.overlay(NSPickerConfigurator { // << here !!
$0.segmentDistribution = .fillEqually // change style !!
})
.pickerStyle(SegmentedPickerStyle())
and configurator itself
struct NSPickerConfigurator: NSViewRepresentable {
var configure: (NSSegmentedControl) -> Void
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
if let holder = view.superview?.superview {
let subviews = holder.subviews
if let nsSegmented = subviews.first?.subviews.first as? NSSegmentedControl {
self.configure(nsSegmented)
}
}
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
Ah the reach down to AppKit method.
Very clever indeed.
However this is not working for me, Monteray 12.3
Went to debug further using Xcode's Visual Debugger and I can see the NSPickerConfigurator class in the view hierarchy but no NSSegmetedControl.
It appears as if apple is clearing up NSViews from the hierarchy.
Time to think pure swiftui.

How to get accentColor background for menu items in SwiftUI with reduced transparency?

Using AppKit you can add NSMenuItems to an NSMenu. I saw that there is something similar to NSMenu in SwiftUI, namely MenuButton. But I cannot find any documentation on how it works.
I tried the following:
MenuButton("+") {
Button("New contact") { print("Create new contact") }
Button("New group") { print("Create new group") }
}
And that gives me this
It looks almost OK but when I enable the "Reduce transparency" in system preferences
The buttons have a different background color than the menu (notice the slightly lighter color above and beneath the menu items).
When I hover the menu items, their background color doesn't change like a normal macOS menu. See the image below:
I also tried to change the background color manually using the .background() modifier but that doesn't affect the full width of the menu item.
MenuButton("+") {
Button("New contact") { print("Create new contact") }
.background(Color.accentColor)
Button("New group") { print("Create new group") }
}
I suppose this is because I am placing Buttons inside the MenuButton while it is probably expecting some other SwiftUI element. What elements should I place inside MenuButtons to create a normal looking macOS menu like the one below?
[Update] macOS Big Sur
I also tried this out in Big Sur. While the background renders correctly, in Big Sur, the text color is messed up now. 🤯
I think I found a partial solution to this by configuring a ButtonStyle and applying it to the MenuButton generic structure. Note however that the conditional change of .foregroundColor isn't inherited by the individual's Button()'s Text(). Also the color ain't right.
Perhaps someone wants to improve on this.
struct DetectHover: ButtonStyle {
#State private var hovering: Bool = false
public func makeBody(configuration: DetectHover.Configuration) -> some View {
configuration.label
.foregroundColor(self.hovering ? Color.white : Color.primary)
.background(self.hovering ? Color.blue : Color.clear)
.onHover { hover in
self.hovering = hover
}
}
}
MenuButton(label: Image(nsImage: NSImage(named: NSImage.actionTemplateName)!)) {
// Buttons
}.buttonStyle(DetectHover())
I’ve got something that nearly looks right. I have created a custom button which does change appearance when hovered.
I am very new to both Swift and SwiftUI, so the following may be clumsy. I would welcome any improvements.
struct HoverButton: View {
var text = "Hover Button"
var action = {}
#State private var hovering = false
var body: some View {
Button(text, action: action )
.padding(.horizontal, 6)
.padding(.vertical, 2)
.buttonStyle(PlainButtonStyle())
.frame(minWidth: 0, maxWidth: .infinity)
.background(self.hovering ? Color(.selectedMenuItemColor) : Color(.clear))
.onHover { hover in
self.hovering = hover
}
}
}
// Usage:
MenuButton("Test") {
HoverButton(text: "Apple", action: {
print("Apple")
})
HoverButton(text: "Banana", action: {
print("Banana")
})
}
.font(.system(size: 14, /* weight: .heavy, */ design: .default))
.menuButtonStyle(BorderlessButtonMenuButtonStyle())
The whole point in creating the custom button is to have access to Self so that I can change its appearance. However, there are some serious shortcomings:
The main problem is that the buttons don’t take the full width of the menu body. I have no idea how to fix that.
The menu buttons are centered. I tried using a HStack with a Spacer() but that didn’t help.
I have no idea how to omit the first parameter name as for a real Button

Remote Image loads correctly using List, but does not load when using ScrollView with embeded VStack with SwitUI

I am fetching a remote image inside a List. This works correctly.
But It does not work if I place the code inside a ScrollView
ScrollView {
HStack {
I have provided the sample XCode 11-Beta SwiftUI project here. Just download and run. Once downloaded, look at the ContentView.swift file. (see image)
Comment the part that does not work, and uncomment the part that works to see the difference. List works, but scrollView fails.
Any Idea why?
[UPDATE]:
I have now changed the code on the struct BuggedView to the following:
struct BuggedView : View {
#ObjectBinding var viewModel = EventController.ViewModel()
var body: some View {
ZStack(alignment: .top) {
GeometryReader() { x in
ScrollView {
ForEach(0..<self.viewModel.events.count) { index in
EventComponent(event: self.viewModel.events[index]).padding(.top).frame(width: x.size.width)
}
}
}
List {
Spacer().frame(height: 183)
ForEach(0..<self.viewModel.events.count) { index in
EventComponent(event: self.viewModel.events[index]).padding(.top)
}
}.offset(x: 200, y: 0)
}//.frame(width:400, height: 400)
}
}
With this change, you can see both versions of the image on the screen at the same time. In the updated code, notice i have some commented out Frame code. Well here are the result
With frame code commented - It Does not work
With frame code un-commented - It Does work
SwiftUI bug maybe?
This is in almost certainly a SwiftUI bug to report to Apple that should be fixed at some point. The .frame modifier should have no effect like you're seeing.