I have a strange behavior on my SwiftUI app. I draw a Path with a linear gradient as stroke color.
This path is displayed like this in my view:
GeometryReader { proxy in
LinePath(width: proxy.size.width, height: proxy.size.height)
.stroke(gradient, lineWidth: 3)
}
.frame(height:200)
.padding()
(LinePath struct extends Shape and draw the Path)
When this code is displayed on first view, it draws correctly:
But if I display it in a second view after a NavigationLink, the drawn line is "pixelated":
As it may be difficult to understand and there is quite lot of code (for a web page), I did a demo project you can get here.
To test it, you can open testprojectApp.swift and comment/uncomment parts:
ContentView() // <--- Uncomment this to get the issue
//SubView() // <--- Uncomment this and it works
If you call SubView (where this LinePath is drawn), it works. If you call ContentView that will display a button to navigate to SubView, it draws a pixelated line.
Notes:
If I don't set a height, it is never pixelated (.frame(height:200)).
If I don't use a gradient but a simple color (as .blue), it is never pixelated.
EDIT: I've found a "workaround", to redraw the line 0.01s after it appeared and change its height. It visually "works" but of course it is not a good method...
#State var height: CGFloat = 0
var body: some View {
GeometryReader { proxy in
LinePath(width: proxy.size.width, height: proxy.size.height)
.stroke(gradient, lineWidth: 3)
}
.frame(height:height)
.padding()
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
height = 200
}
}
}
You can add .drawingGroup() and it seems to resolve the issue without having to use the delay:
GeometryReader { proxy in
LinePath(width: proxy.size.width, height: proxy.size.height)
.stroke(gradient, lineWidth: 3)
}
.frame(height:200)
.padding()
.drawingGroup() //<-- here
Related
Well, honestly, I did it, because I needed it, and only then looked around and did not find anything on SO native in SwiftUI, so wanted to share. Thus this is just a self-answered question.
Initially I needed sticky stretchable sticky header for lazy content dependent only on ScrollView.
Later (after I got my solution) I found this one on Medium, but I don't like it (and would not recommend at least as-is), because:
overcomplicated (many unneeded code, many unneeded calculations)
depends (and joins) with safe area only, so limited applicability
based on offset (I don't like to use offset, because of its inconsistency with layout, etc.)
it is not sticky and to make it sticky it is needed even more code
So, actually all this text was just to fulfil SO question requirements - who knows me here knows that I don't like to type many text, it is better to type code 😀, in short - my approach is below in answer, maybe someone find it useful.
Initial code which SwiftUI gives us for free
ScrollView {
LazyVStack(spacing: 8, pinnedViews: [.sectionHeaders]) {
Section {
ForEach(0...100) {
Text("Item \($0)")
.frame(maxWidth: .infinity, minHeight: 60)
}
} header: {
Image("picture").resizable().scaledToFill()
.frame(height: 200)
}
}
}
Header is sticky by scrolling up, but not when down (dragged with content), and it is not stretchable.
iOS 15.5 (initial)
Ok, we need to solve two problems:
make top of header pinned to top of ScrollView on drag down
stretch header on drag down to make header content (image in majority of cases) scale to fill
A possible approach to solve this:
ScrollView now manages content offsets privately (UIKit variants are out of topics here), so to pin to top using overlay
ScrollView {
// ...
}
.overlay(
// >> any header
Image("picture").resizable().scaledToFill()
// << header end
.frame(height: imageHeight) // will calculate below
.clipped()
Use Section default header (as placeholder) to calculate current distance from ScrollView top
Section(...) {
// ...
} header: {
// here is only caculable part
GeometryReader {
// detect current position of header bottom edge
Color.clear.preference(key: ViewOffsetKey.self,
value: $0.frame(in: .named("area")).maxY)
}
.frame(height: headerHeight)
.onPreferenceChange(ViewOffsetKey.self) {
// prevent image negative height if header is not pinned
// for simplicity (can be optional, etc.)
imageHeight = $0 < 0 ? 0.001 : $0
}
}
That's actually it, everything else is just for demo part.
Tested with Xcode 13.4 / iOS 15.5
Test module is here
If you need solution based on ScrollView:
1/ find scrollOffset (see example or use ScrollViewWithScrollOffset)
2/ wrap image with GeometryReader to avoid frame glitches and get normal image size on device screen
3/ use size from GeometryReader and scrollOffset to set image frame
Full code:
struct ContentView: View {
#State var scrollOffset: CGFloat = 0
private let coordinateSpaceName = "scrollViewSpaceName"
var body: some View {
ScrollView {
VStack {
image
Color.gray.frame(height: 1000)
}
.background( // 1. find scrollOffset
GeometryReader { proxy in
let offset = proxy.frame(in: .named(coordinateSpaceName)).minY
Color.clear.preference(key: ScrollViewWithPullDownOffsetPreferenceKey.self, value: offset)
}
)
}
.coordinateSpace(name: coordinateSpaceName)
.onPreferenceChange(ScrollViewWithPullDownOffsetPreferenceKey.self) { value in
scrollOffset = value
}
}
var image: some View { frame
GeometryReader { proxy in // 2. get actual size on screen
Image(systemName: "heart.fill") // 3. use scrollOffset to adjust image
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.horizontal, min(0, -scrollOffset))
.frame(width: proxy.size.width,
height: proxy.size.height + max(0, scrollOffset))
.offset(CGSize(width: 0, height: min(0, -scrollOffset)))
}
.aspectRatio(CGSize(width: 375, height: 280), contentMode: .fit)
}
}
If the .cornerRadius modifier comes after the .frame modifier, the image becomes much slower. What is the reason behind this ?
struct ContentView: View {
var body: some View {
VStack(spacing: 100) {
Image("image1")
.resizable()
.scaledToFill()
.frame(width: 343, height: 184)
.cornerRadius(8)
Image("image1")
.resizable()
.scaledToFill()
.cornerRadius(8)
.frame(width: 343, height: 184)
}
}
}
In SwiftUI the order of modifiers is important and can lead to a different output.
A rule of thumb is:
read them from bottom to top
understand that every modifier produce a new view
the modifier "modifies" only what's below:
-> .frame(width: 343, height: 184)
-> .cornerRadius(8)
-> .scaledToFill()
-> .resizable()
Since you used the cornerRadius the documentation states that:
Clips this view to its bounding frame, with the specified corner
radius.
Behind the scene, you can imagine the engine applying two modifiers, the clipped and the corner radius.
Because every modifier produces a new view when the clipping is calculated it's like it doesn't know what are the bounds in which constrain the new view and that explains why the image goes out.
If we go a step further where we try to apply two frame and cornerRadius hopefully should be clearer how SwiftUI interprets the modifiers:
If you want to know more, this discussion goes deeper into the details of how SwiftUI sees the modifier: Order of modifiers in SwiftUI view impacts view appearance
I noticed that a Text with .italic() clips letters:
Setting frame size doesn't help:
.paddings() doesn't help either. kerning(5) I don't want to use as it fixes the problem partially, at the right edge only, but it adds unwanted letter spacing.
struct ItalicTest: View {
var body: some View {
Text("F")
.font(Font.system(size: 60))
.italic()
.fontWeight(.black)
.frame(width: 60, height: 60)
.background(Color.red)
}
}
I'd like to prevent clipping. Do you know a solution using pure SwiftUI?
I know this is an old question, but I just had the same issue and found a propper solution.
You will have to add padding to your text and ask swiftui to collapse the padding and text before rendering it.
struct ItalicTest: View {
var body: some View {
Text("F")
.font(Font.system(size: 60))
.italic()
.fontWeight(.black)
.padding(.horizontal) // <-- add padding
.drawingGroup() // collapse the view and render together
}
}
I've been wondering if there is any way to customize the preview image of the view that's being dragged when using onDrag?
As you might see, the preview image is by default a slightly bigger opacity image of the view.
From what I have found, a preview image is generated at the very beginning of the dragging process. But I couldn't find a way to change it.
What I mean by customizing is to have some custom image or a preview image of a custom view. (Both without the default opacity)
Does anyone have an idea?
I have tried to use previewImageHandler and in general, read a lot about the NSItemProvider. But for me, it seems like this is something that is not possible for SwiftUI yet?
With UIKit one could have just customized the UIDragItem - something like that using previewProvider: Here
Here is my demo code:
struct ContentView: View {
var body: some View {
DraggedView()
.onDrag({ NSItemProvider() })
}
private struct DraggedView: View {
var body: some View {
RoundedRectangle(cornerRadius: 20)
.frame(width: 120, height: 160)
.foregroundColor(.green)
}
}
}
I will use this for drag and drop within a LazyVGrid, so custom gestures are unfortunately no option.
One second idea I had would be to have a gesture simultaneously that first changes the item to be dragged to something else and then onDrag starts and returns the NSItemProvider with the preview image which would be then the one I would want. But I couldn't have those two gestures go at the same time, you would have to dismiss one first in order to start the second.
Thank you!
iOS 15 adds an API to do this - you can specify the View to use for the preview. onDrag(_:preview:)
RoundedRectangle(cornerRadius: 20)
.frame(width: 120, height: 160)
.foregroundColor(.green)
.onDrag {
NSItemProvider()
} preview: {
RoundedRectangle(cornerRadius: 18)
.frame(width: 100, height: 140)
.foregroundColor(.green)
}
So I'm just trying to build a list that look like below, however, the SwiftUI Button drive me crazy and I wanna make corner radius 10, but the corner actually always disappears.
This is the row I want, notice that the 'Follow' button has cornerRadius and a proper height
However after I searched tons of damn answer, what I can only got is this, the corner even disappears!!!:
Button(action: {
}) {
if person.isInKnock {
Text("Follow").font(.system(size: 14)).foregroundColor(Color(ColorUtils.hexStringToUIColor(hex: Constants.THEME.THEME_COLOR))).padding()
}
else {
Text("Invite to Knock").font(.system(size: 14)).foregroundColor(Color(ColorUtils.hexStringToUIColor(hex: Constants.THEME.THEME_COLOR))).padding()
}
}.frame(height: CGFloat(30)).border(Color.gray, width: CGFloat(1)).cornerRadius(CGFloat(10))
Apple has been switching up the modifier functions constantly, so it's annoying to keep up with all the changes. Here's a working solution I've found:
Button(action: {}) {
Text("Follow")
.foregroundColor(Color(.systemTeal))
.bold()
.padding([.leading, .trailing])
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray)
.foregroundColor(.clear)
)
}
This produces the following image:
The key is overlaying a RoundedRectangle and changing the stroke and corner radius. .stroke() changes the shape itself, while .border() changes the view of the rectangle. Hope this helps!