I need to implement an onboarding like in the image below. I can't apply blur avoiding the icon. I know how to do it using UIViewRepresentable, but I want to reach the goal using SwiftUI 2.0(min iOS 14.0). Is there a way to create such a masked blur without UIViewRepresentable?
UPD. I have a view hierarchy. I need to blur it(a gaussian blur effect with radius 5) and cover it with a black tint with an opacity 0.3 and a mask with a hole. Everything is ok, but the blur modifier also applies an effect to an element from the hole. It must not be blurred(like "Flag" icon at the screenshot). I can't separate this element from the view hierarchy. This onboarding view modifier must be reusable across the app.
There are two ways of doing so.
Either use the blur just on the background:
struct ContentView: View {
var body: some View {
VStack {
Icon()
Spacer()
}.background(Image("cat").blur(radius: 2.5))
}
}
Or, when having a entire view in the background you can use the ZStack and just blur the View you want to be blurred. Make sure that the View/ Element you don't want blurred is above. Like so:
struct ContentView2: View {
var body: some View {
ZStack {
Image("cat").blur(radius: 2.5)
VStack {
Icon()
Spacer()
}
}
}
}
I used both times a entire View for the Icon which is probably a little over engineered but it gives you the idea:
struct Icon: View {
var body: some View {
Image(systemName: "pencil.circle.fill")
.resizable()
.frame(width: 50, height: 50)
.padding()
.foregroundColor(.red)
}
}
Both giving you this as result:
I found a solution to my problem. I used some code from other answers and wrote my OnboardingViewModifier. I used content from ViewModifier body function twice in a ZStack. The first one is an original view, the second one is blurred and masked. It gave me the needed result.
extension Path {
var reversed: Path {
let reversedCGPath = UIBezierPath(cgPath: cgPath)
.reversing()
.cgPath
return Path(reversedCGPath)
}
}
struct ShapeWithHole: Shape {
let hole: CGRect
let cornerRadius: CGFloat
func path(in rect: CGRect) -> Path {
var path = Rectangle().path(in: rect)
path.addPath(RoundedRectangle(cornerRadius: cornerRadius).path(in: hole).reversed)
return path
}
}
struct OnboardingViewModifier<DescriptionView>: ViewModifier where DescriptionView: View {
let hole: CGRect
let isPresented: Bool
#ViewBuilder let descriptionOverlay: () -> DescriptionView
func body(content: Content) -> some View {
content
.disabled(isPresented)
.overlay(overlay(content))
}
#ViewBuilder
func overlay(_ content: Content) -> some View {
if isPresented {
ZStack {
content
.blur(radius: 5)
.disabled(isPresented)
Color.black.opacity(0.3)
descriptionOverlay()
}
.compositingGroup()
.mask(ShapeWithHole(hole: hole, cornerRadius: 25))
.ignoresSafeArea(.all)
}
}
}
extension View {
func onboardingWithHole<DescriptionView>(
isPresented: Bool,
hole: CGRect,
#ViewBuilder descriptionOverlay: #escaping () -> DescriptionView) -> some View where DescriptionView: View {
modifier(OnboardingViewModifier(hole: hole, isPresented: isPresented, descriptionOverlay: descriptionOverlay))
}
}
Related
Reacting to the user’s device rotation and taking into account the various screen sizes of iPhones and iPads i want two (for example) Text() Views to have the maximum possible font size without truncating and without line wrapping. I tried a lot, lastly this and nothing worked.
struct MinimumHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 1_000.0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = min(value, nextValue())
}
}
struct DetermineHeight: View{
typealias Key = MinimumHeightPreferenceKey
var body: some View {
GeometryReader { proxy in
Color.clear
.anchorPreference(key: Key.self, value: .bounds) {
anchor in proxy[anchor].size.height
}
}
}
}
struct ContentView: View {
#State var minTextHeight: CGFloat = 75
var body: some View {
VStack {
HStack(alignment: VerticalAlignment.firstTextBaseline){
Text("Short Text")
.frame(maxHeight: minTextHeight)
.overlay(DetermineHeight())
.border(Color.red)
.scaledToFit()
Text("This is a considerably longer text. ideally it should also reduce the shorter Text's size so they both look the same.")
//.frame(minHeight: 1.0, maxHeight: minTextHeight)
.overlay(DetermineHeight())
.border(Color.green)
}
.font(.title)
.lineLimit(1)
.minimumScaleFactor(0.25)
.onPreferenceChange(DetermineHeight.Key.self) {
minTextHeight = $0
}
Text("minHeight = \(minTextHeight)")
}
}
}
The boxes turn out the same hight, but the texts aren’t adjusted. (Plus: if I uncomment that line I the boxes don’t resize at all. Huh?)
Am I trying to do the impossible?
With the new ScrollViewReader, it seems possible to set the scroll offset programmatically.
But I was wondering if it is also possible to get the current scroll position?
It seems like the ScrollViewProxy only comes with the scrollTo method, allowing us to set the offset.
Thanks!
It was possible to read it and before. Here is a solution based on view preferences.
struct DemoScrollViewOffsetView: View {
#State private var offset = CGFloat.zero
var body: some View {
ScrollView {
VStack {
ForEach(0..<100) { i in
Text("Item \(i)").padding()
}
}.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("scroll")).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) { print("offset >> \($0)") }
}.coordinateSpace(name: "scroll")
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
I found a version without using PreferenceKey. The idea is simple - by returning Color from GeometryReader, we can set scrollOffset directly inside background modifier.
struct DemoScrollViewOffsetView: View {
#State private var offset = CGFloat.zero
var body: some View {
ScrollView {
VStack {
ForEach(0..<100) { i in
Text("Item \(i)").padding()
}
}.background(GeometryReader { proxy -> Color in
DispatchQueue.main.async {
offset = -proxy.frame(in: .named("scroll")).origin.y
}
return Color.clear
})
}.coordinateSpace(name: "scroll")
}
}
I had a similar need but with List instead of ScrollView, and wanted to know wether items in the lists are visible or not (List preloads views not yet visible, so onAppear()/onDisappear() are not suitable).
After a bit of "beautification" I ended up with this usage:
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
List(0..<100) { i in
Text("Item \(i)")
.onItemFrameChanged(listGeometry: geometry) { (frame: CGRect?) in
print("rect of item \(i): \(String(describing: frame)))")
}
}
.trackListFrame()
}
}
}
which is backed by this Swift package: https://github.com/Ceylo/ListItemTracking
The most popular answer (#Asperi's) has a limitation:
The scroll offset can be used in a function
.onPreferenceChange(ViewOffsetKey.self) { print("offset >> \($0)") }
which is convenient for triggering an event based on that offset.
But what if the content of the ScrollView depends on this offset (for example if it has to display it). So we need this function to update a #State.
The problem then is that each time this offset changes, the #State is updated and the body is re-evaluated. This causes a slow display.
We could instead wrap the content of the ScrollView directly in the GeometryReader so that this content can depend on its position directly (without using a State or even a PreferenceKey).
GeometryReader { geometry in
content(geometry.frame(in: .named(spaceName)).origin)
}
where content is (CGPoint) -> some View
We could take advantage of this to observe when the offset stops being updated, and reproduce the didEndDragging behavior of UIScrollView
GeometryReader { geometry in
content(geometry.frame(in: .named(spaceName)).origin)
.onChange(of: geometry.frame(in: .named(spaceName)).origin,
perform: offsetObserver.send)
.onReceive(offsetObserver.debounce(for: 0.2,
scheduler: DispatchQueue.main),
perform: didEndScrolling)
}
where offsetObserver = PassthroughSubject<CGPoint, Never>()
In the end, this gives :
struct _ScrollViewWithOffset<Content: View>: View {
private let axis: Axis.Set
private let content: (CGPoint) -> Content
private let didEndScrolling: (CGPoint) -> Void
private let offsetObserver = PassthroughSubject<CGPoint, Never>()
private let spaceName = "scrollView"
init(axis: Axis.Set = .vertical,
content: #escaping (CGPoint) -> Content,
didEndScrolling: #escaping (CGPoint) -> Void = { _ in }) {
self.axis = axis
self.content = content
self.didEndScrolling = didEndScrolling
}
var body: some View {
ScrollView(axis) {
GeometryReader { geometry in
content(geometry.frame(in: .named(spaceName)).origin)
.onChange(of: geometry.frame(in: .named(spaceName)).origin, perform: offsetObserver.send)
.onReceive(offsetObserver.debounce(for: 0.2, scheduler: DispatchQueue.main), perform: didEndScrolling)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.coordinateSpace(name: spaceName)
}
}
Note: the only problem I see is that the GeometryReader takes all the available width and height. This is not always desirable (especially for a horizontal ScrollView). One must then determine the size of the content to reflect it on the ScrollView.
struct ScrollViewWithOffset<Content: View>: View {
#State private var height: CGFloat?
#State private var width: CGFloat?
let axis: Axis.Set
let content: (CGPoint) -> Content
let didEndScrolling: (CGPoint) -> Void
var body: some View {
_ScrollViewWithOffset(axis: axis) { offset in
content(offset)
.fixedSize()
.overlay(GeometryReader { geo in
Color.clear
.onAppear {
height = geo.size.height
width = geo.size.width
}
})
} didEndScrolling: {
didEndScrolling($0)
}
.frame(width: axis == .vertical ? width : nil,
height: axis == .horizontal ? height : nil)
}
}
This will work in most cases (unless the content size changes, which I don't think is desirable). And finally you can use it like that :
struct ScrollViewWithOffsetForPreviews: View {
#State private var cpt = 0
let axis: Axis.Set
var body: some View {
NavigationView {
ScrollViewWithOffset(axis: axis) { offset in
VStack {
Color.pink
.frame(width: 100, height: 100)
Text(offset.x.description)
Text(offset.y.description)
Text(cpt.description)
}
} didEndScrolling: { _ in
cpt += 1
}
.background(Color.mint)
.navigationTitle(axis == .vertical ? "Vertical" : "Horizontal")
}
}
}
Is this layout possible with SwiftUI?
I want the first column to wrap the size of the labels, so in this case it will be just big enough to show "Bigger Label:". Then give the rest of the space to the second column.
This layout is pretty simple with auto layout.
SwiftUI 2020 has LazyVGrid but the only ways I see to set the column sizes use hardcoded numbers. Do they not understand what a problem that causes with multiple languages and user-adjustable font sizes?
It is not so complex if to compare number of code lines to make this programmatically in both worlds...
Anyway, sure it is possible. Here is a solution based on some help modifier using view preferences feature. No hard. No grid.
Demo prepared & tested with Xcode 12 / iOS 14.
struct DemoView: View {
#State private var width = CGFloat.zero
var body: some View {
VStack {
HStack {
Text("Label1")
.alignedView(width: $width)
TextField("", text: .constant("")).border(Color.black)
}
HStack {
Text("Bigger Label")
.alignedView(width: $width)
TextField("", text: .constant("")).border(Color.black)
}
}
}
}
and helpers
extension View {
func alignedView(width: Binding<CGFloat>) -> some View {
self.modifier(AlignedWidthView(width: width))
}
}
struct AlignedWidthView: ViewModifier {
#Binding var width: CGFloat
func body(content: Content) -> some View {
content
.background(GeometryReader {
Color.clear
.preference(key: ViewWidthKey.self, value: $0.frame(in: .local).size.width)
})
.onPreferenceChange(ViewWidthKey.self) {
if $0 > self.width {
self.width = $0
}
}
.frame(minWidth: width, alignment: .trailing)
}
}
Is there a way to set up the background of the whole app (same default background for each view) in one place? For example in the SceneDelegate?
Create a custom ViewModifier, throw in your color, and add it to your views. For instance, if you want all your views to be orange, do this:
struct BackgroundColorStyle: ViewModifier {
func body(content: Content) -> some View {
return content
.background(Color.orange)
}
}
And usage is:
Text("Hello world!").modifier(BackgroundColorStyle())
Now, you can - and probably should - expand on this for light/dark mode. In this case, you can use the environment variable ColorSchmem:
struct BackgroundColorStyle: ViewModifier {
#Environment (\.colorScheme) var colorScheme:ColorScheme
func body(content: Content) -> some View {
if colorScheme == .light {
return content
.background(Color.darkGrey)
} else {
return content
.background(Color.white)
}
}
}
Either way, every View using this modifier has their background color defined in one place. If you wish to define a border along with a background color, same thing.
import SwiftUI
struct TestView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.blue)
.edgesIgnoringSafeArea(.all)
Text("Hello World!")
.foregroundColor(.white)
}
}
}
ZStack and Rectangle(), Setting foregroundColor and edgesIgnoringSafeArea
I just started with SwiftUI, and seems VStack and HStack is very similar as flex box in web. On the web, it's easy to split two sub views as height weight with flex
<div id="parent" style="display: flex; flex-direction: column; height: 300px">
<div id="subA" style="flex: 1; background-color: red">Subview A</div>
<div id="subB" style="flex: 2; background-color: yellow">Subview B</div>
</div>
I wonder if it's possible on swiftUI too.
VStack {
VStack {
Text("Subview A")
} // Subview A with height 100
.background(Color.red)
VStack {
Text("Subview B")
} // Subview B with height 200
.background(Color.yellow)
}
.frame(height: 300, alignment: .center)
How can I implement that?
UPDATE #2:
Thanks to this answer and code from #kontiki, here's what easily works instead of using this deprecated method:
Declare this:
#State private var rect: CGRect = CGRect()
Then create this:
struct GeometryGetter: View {
#Binding var rect: CGRect
var body: some View {
GeometryReader { geometry in
Group { () -> ShapeView<Rectangle, Color> in
DispatchQueue.main.async {
self.rect = geometry.frame(in: .global)
}
return Rectangle().fill(Color.clear)
}
}
}
}
(For those familiar with UIKit, you are basically creating an invisible CALayer or UIView in the parent and passing it's frame to the subview - apologies for not being 100% technically accurate, but remember, this is not a UIKit stack in any way.)
Now that you have the parent frame, you can use it as a base for a percentage - or "relative" - of it. In this question there's a nested VStack inside another and you want the lower Text to be twice the vertical size of the top one. In the case of this answer, adjust your `ContentView to this:
struct ContentView : View {
#State private var rect: CGRect = CGRect()
var body: some View {
VStack (spacing: 0) {
RedView().background(Color.red)
.frame(height: rect.height * 0.25)
YellowView()
}
.background(GeometryGetter(rect: $rect))
}
}
UPDATE #1:
As of beta 4, this method is deprecated. relativeHeight, relativeWidth, relativeSizehave all been deprecated. Useframeinstead. If you want *relatve* sizing based on aView's parent, use GeometryReader` instead. (See this question.)
ORIGINAL POST:
Here's what you want. Keep in mind that without modifiers, everything is centered. Also, relativeHeight seems (at least to some) not very intuitive. The key is to remember that in a VSTack the parent is 50% of the screen, so 50% of 50% is actually 25%.
Alternatively, you can dictate frame heights (letting the width take up the whole screen). but your example suggests you want the red view to be 25% of the entire screen no matter what the actual screen size is.
struct RedView: View {
var body: some View {
ZStack {
Color.red
Text("Subview A")
}
}
}
struct YellowView: View {
var body: some View {
ZStack {
Color.yellow
Text("Subview B")
}
}
}
struct ContentView : View {
var body: some View {
VStack (spacing: 0) {
RedView().background(Color.red).relativeHeight(0.50)
YellowView()
}
}
}