Can I generate a sequence of Views programatically in SwiftUI? - ios

Here is a fragment of code that works for me...
VStack(spacing: pad/2) {
getRgbColor(i:0)
.onTapGesture { editColor(i:0) }
.frame(width: pad, height: pad)
.border(.black.opacity(0.5), width: 1)
getRgbColor(i:1)
.onTapGesture { editColor(i:1) }
.frame(width: pad, height: pad)
.border(.black.opacity(0.5), width: 1)
getRgbColor(i:2)
.onTapGesture { editColor(i:2) }
.frame(width: pad, height: pad)
.border(.black.opacity(0.5), width: 1)
}
The original had more entries. It worked but you can see how it repeats. I may want to make a 6x4 set of patches that looks like a Macbeth target, so I wondered whether there was a better way that did not repeat so much. I tried moving the calls into the getRgbColor() call..
func getRgbColor(i:Int) -> SwiftUI.Color {
return SwiftUI.Color(red: R + rgbDel[i][0] * step,
green: G + rgbDel[i][1] * step,
blue: B + rgbDel[i][2] * step)
.onTapGesture { editColor(i:0) }
.frame(width: pad, height: pad)
.border(.black.opacity(0.5), width: 1) as! Color
}
This compiled but gave a SIGABRT when I ran it...
Could not cast value of type
'SwiftUI.ModifiedContent<SwiftUI.ModifiedContent<SwiftUI.ModifiedContent<SwiftUI.Color,
SwiftUI.AddGestureModifier<SwiftUI._EndedGesture<SwiftUI.TapGesture>>>,
SwiftUI._FrameLayout>,
SwiftUI._OverlayModifier<SwiftUI._ShapeView<SwiftUI._StrokedShape<SwiftUI.Rectangle._Inset>,
SwiftUI.Color>>>' (0x109735e90) to 'SwiftUI.Color' (0x1ec3b9eb8).
2022-10-01 12:38:49.468445+0100 ByEye[2810:493288] Could not cast
value of type
'SwiftUI.ModifiedContent<SwiftUI.ModifiedContent<SwiftUI.ModifiedContent<SwiftUI.Color,
SwiftUI.AddGestureModifier<SwiftUI._EndedGesture<SwiftUI.TapGesture>>>,
SwiftUI._FrameLayout>,
SwiftUI._OverlayModifier<SwiftUI._ShapeView<SwiftUI._StrokedShape<SwiftUI.Rectangle._Inset>,
SwiftUI.Color>>>' (0x109735e90) to 'SwiftUI.Color' (0x1ec3b9eb8).
Is there a neat solution?
Yes there is. I hadn't imagined that calling the modifier changed the underlying type, because most of what I write is in cpp, where this is unthinkable. Following the first comment, I wrote and re-named the getRgbColor() function...
func rgbButton(i:Int) -> some View {
return SwiftUI.Color(red: R + rgbDel[i][0] * step,
green: G + rgbDel[i][1] * step,
blue: B + rgbDel[i][2] * step)
.onTapGesture { editColor(i:i) }
.frame(width: pad, height: pad)
}

Modifiers in SwiftUI don't actually modify the view. Instead, they create a new view (e.g. ModifiedContent) with the property changed/applied.
Whenever we apply a modifier to a SwiftUI view, we actually create a new view with that change applied – we don’t just modify the existing view in place.
- Hacking With Swift
So when you add all the modifiers to the Color, you are making it a different type of view. That's why as! Color fails.
To fix that, you could make a View for the rgb color.
struct RgbColor: View {
let i: Int
// Add any other info the view needs as constants
var body: some View {
// ...
}
}
To use it:
RgbColor(i: 1, /* other parameters */)
I hope this helps!

Related

How to ignore safe area for container and respect safe area for content in swift UI

I'm trying to create a bottom sheet in swift ui that looks something like this
https://user-images.githubusercontent.com/33900517/172068237-4dd58374-b6e6-4340-a913-7085fb64b254.mp4
My issue is that I have an animated bottom sheet, but because it is ignoring the safe area, when I click into the textfield it does not expand with the keyboard.
How would I fix this so the view expands with the keyboard but the white at the bottom still goes beyond the safe area?
I.e. the containing view should ignore the safe area, and the content within should adhere to the safe area.
Here is the bottom sheet code snippet, full example can be found here
https://gist.github.com/CTOverton/4fbfb8db2de31f3b5f5ef9ee88e8f744
var body: some View {
GeometryReader { geometry in
VStack() {
self.content
}
.padding(.vertical, 34)
.padding(.horizontal, 16)
// .frame(width: geometry.size.width, height: geometry.size.height * heightRatio, alignment: .top)
.frame(width: geometry.size.width, height: self.maxHeight, alignment: .top)
.background(Color(.white))
.cornerRadius(Constants.radius)
.frame(height: geometry.size.height, alignment: .bottom)
.offset(y: max(self.offset + self.translation, 0))
.animation(.interactiveSpring())
.gesture(
DragGesture().updating(self.$translation) { value, state, _ in
state = value.translation.height
}.onEnded { value in
let snapDistance = self.maxHeight * Constants.snapRatio
guard abs(value.translation.height) > snapDistance else {
return
}
self.isOpen = value.translation.height < 0
}
)
}
.edgesIgnoringSafeArea([.bottom, .horizontal])
.shadow(color: Color(hue: 1.0, saturation: 0.0, brightness: 0.0, opacity: 0.08), radius: 12, y: -8)
}
I've tried various configurations of .ignoreSafeArea() and .safeAreaInset() but I just can't seem to get it quite right.
Here are some pictures for reference as well
Actually instead of ignoring safe area for everything (that results in issue), we need it only in background, so the question is how to correctly construct background in this case.
Note: the .cornerRadius is also not appropriate here, because it clips content
Here is a main part of a fix. Tested with Xcode 13.4 / iOS 15.5
.background(
RoundedRectangle(cornerRadius: Constants.radius) // corners !!
.fill(.white) // background !!
.edgesIgnoringSafeArea([.bottom, .horizontal]) // << only here !!
)
Complete test module is here

SwiftUI Path is pixelated when in second Navigation View

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

How to get a grid in SwiftUI with custom layout (different cell size)

So I am trying to get a custom grid with cells of different size. I have kinda gotten stuck here. So I have one particular cell with the height and size double that of all the others. But the left side of the cell requires two rows instead of one. How can I force to add two more boxes below H3 or to left of H4 box in the picture below.
Here is my code:
struct CustomGridView: View {
var body: some View {
let gridItems = [GridItem(.fixed(150), spacing: 10, alignment: .leading),
GridItem(.fixed(150), spacing: 10, alignment: .leading),
GridItem(.fixed(150), spacing: 10, alignment: .leading)]
LazyVGrid(columns: gridItems, spacing: 10) {
ForEach(0..<9) { g in
Text("H:\(g)")
.frame(width: g == 4 ? 310 : 150, height: g == 4 ? 310 : 150)
.background(Color.red)
if g == 4 { Color.clear }
}
}
.frame(width: 470)
}
}
Here is the screen shot of the grid situation so far:
Any help with this will be appreciated.
I don't think you can do that when just using lazy vgrid ForEach... Perhaps you can do something like that if you check for highest cell height in a row and then somehow fill it with two more boxes. You'd need to write a custom function for that. Also which block will go into the blank space above/below H:3? You'd need to write some custom logic for that.

SwiftUI | customize dragitem appearance within LazyGrid using onDrag [duplicate]

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)
}

Array.forEach creates error "Cannot convert value of type '()' to closure result type '_'"

I am trying to loop through an array in SwuftUI to render multiple text strings in different locations. Going through the array manually works, but using forEach-loop produces an error.
In the code sample below, I have commented out the manual approach (which works).
This kind of approach worked in this tutorial for drawing lines (https://developer.apple.com/tutorials/swiftui/drawing-paths-and-shapes)
(As a bonus question: is there a way to get the index/key of the individual positions through this approach, without adding that key in to the positions-Array for each of the rows?)
I have tried various approaches like ForEach, adding identified() in there as well, and adding various type definitions for the closure, but I just end up creating other errors
import SwiftUI
var positions: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 200),
CGPoint(x: 100, y: 300),
]
struct ContentView : View {
var body: some View {
ZStack {
positions.forEach { (position) in
Text("Hello World")
.position(position)
}
/* The above approach produces error, this commented version works
Text("Hello World")
.position(positions[0])
Text("Hello World")
.position(positions[1])
Text("Hello World")
.position(positions[2]) */
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
In the Apple tutorial you pointed to, they used the ´.forEach´ inside the Path closure, which is a "normal" closure. SwiftUI, uses a new swift feature called "function builders". The { brackets } after ZStack might look like a usual closure, but it's not!
See eg. https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api for more about function builders.
In essence, the "function builder" (more specifically, ViewBuilder, in this case; read more: https://developer.apple.com/documentation/swiftui/viewbuilder) get an array of all the statements in the "closure", or rather, their values. In ZStack, those values are expected to be conforming to the View protocol.
When you run someArray.forEach {...}, it will return nothing, void, also known as (). But the ViewBuilder expected something conforming to the View protocol! In other words:
Cannot convert value of type '()' to closure result type '_'
Of course it can't! Then, how might we do a loop/forEach that returns what we want?
Again, looking at the SwiftUI documentation, under "View Layout and Presentation" -> "Lists and Scroll Views", we get: ForEach, which allows us to describe the iteration declaratively, instead of imperatively looping through the positions: https://developer.apple.com/documentation/swiftui/foreach
When a view's state changes, SwiftUI regenerates the struct describing the view, compares it with the old struct, and then only makes the necessary patches to the actual UI, to save performance and allow for fancier animations, etc. To be able to do this, it needs to be able to identify each item in eg. a ForEach (eg. to distinguish an insert of a new point from just a change of an existing one). Thus, we can't just pass the array of CGPoints directly to ForEach (at least not without adding an extension to CGPoint, making them conform to the Identifiable protocol). We could make a wrapper struct:
import SwiftUI
var positions: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 200),
CGPoint(x: 100, y: 300),
]
struct Note: Identifiable {
let id: Int
let position: CGPoint
let text: String
}
var notes = positions.enumerate().map { (index, position) in
// using initial index as id during setup
Note(index, position, "Point \(index + 1) at position \(position)")
}
struct ContentView: View {
var body: some View {
ZStack {
ForEach(notes) { note in
Text(note.text)
.position(note.position)
}
}
}
}
We could then add the ability to tap-and-drag the notes. When tapping a note, we might want to move it to the top of the ZStack. If any animation was playing on the note (for instance, changing its position during drag), it would normally stop (because the whole note-view would be replaced), but because the note struct now is Identifiable, SwiftUI will understand that it's only been moved, and make the change without interfering with any animation.
See https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-views-in-a-loop-using-foreach or https://medium.com/#martinlasek/swiftui-dynamic-list-identifiable-73c56215f9ff for a more in depth tutorial :)
note: the code has not been tested (gah beta Xcode)
Not everything is valid in a view body. If you would like to perform a for-each loop, you need to use the special view ForEach:
https://developer.apple.com/documentation/swiftui/foreach
The view requires an identifiable array, and the identifiable array requires elements to conform to Hashable. Your example would need to be rewritten like this:
import SwiftUI
extension CGPoint: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
}
var positions: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 200),
CGPoint(x: 100, y: 300),
]
struct ContentView : View {
var body: some View {
ZStack {
ForEach(positions.identified(by: \.self)) { position in
Text("Hello World")
.position(position)
}
}
}
}
Alternatively, if your array is not identifiable, you can get away with it:
var positions: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 200),
CGPoint(x: 100, y: 300),
]
struct ContentView : View {
var body: some View {
ZStack {
ForEach(0..<positions.count) { i in
Text("Hello World")
.position(positions[i])
}
}
}
}

Resources