Drawing Rectangles using a ForEach loop SwiftUI - swift

Im trying to create a view of a rectangle with a given amount of rectangles inside of it,
i.e.Image
Im trying to recreate this with the option to pass in a size to add more levels to it, currently I've only managed to overlay
let colors: [Color] = [.red, .green, .blue]
var body: some View {
ZStack {
ForEach(colors, id: \.self) { color in
Text(color.description.capitalized)
.background(color)
}
}
}
which I found only whilst looking for the problem, the only issue I have is having adjustable size values which while using the loop can get progressively smaller, currently the closest I've got is this:
struct SquareContent{
var color: Color
var size: CGSize
}
struct GrannySquareComplete: View {
let colors: [SquareContent] = [
SquareContent.init(color: .red, size: CGSize(width: 100, height: 100)),
SquareContent.init(color: .white, size: CGSize(width: 80, height: 80)),
SquareContent.init(color: .red, size: CGSize(width: 80, height: 80))]
var body: some View {
ZStack {
ForEach(0...colors.count) { i in
Text("")
.frame(width: colors[i].size.width, height: colors[i].size.height)
.background(colors[i].color)
.border(.white)
}
}
}
}
but this returns an error of
No exact matches in call to initialiser
I believe this is due to the use of Text within the foreach loop but cannot figure out how to fix this.

To fix your error change ForEach(0...colors.count) { i in to
ForEach(0..<colors.count) { i in
To make it dynamic
//Make Identifiable
struct SquareContent: Identifiable{
//Add id
let id: UUID = UUID()
var color: Color
//Handle size based on screen
}
struct GrannySquareComplete: View {
//Change to #State
#State var colors: [SquareContent] = [
SquareContent.init(color: .red),
SquareContent.init(color: .white),
SquareContent.init(color: .red)]
var body: some View {
VStack{
//Mimic adding a square
Button("add square"){
colors.append(.init(color: [.blue, .black,.gray, .orange, .pink].randomElement()!))
}
ZStack(alignment: .center){
ForEach(Array(colors.enumerated()), id: \.offset) { (index, color) in
//Calculate the size by percentange based on index
let percentage: CGFloat = Double(colors.count - index)/Double(colors.count)
//Create a custom view to handle the details
SquareComplete(square: color, percentage: percentage)
}
}
}
}
}
struct SquareComplete: View {
let square: SquareContent
let percentage: CGFloat
var body: some View{
GeometryReader{ geo in
Rectangle()
//Make a square
.aspectRatio(1, contentMode: .fit)
.border(.white)
.background(square.color)
.foregroundColor(.clear)
//Center
.position(x: geo.size.width/2, y: geo.size.height/2)
//Adjust size by using the available space and the passed percentage
.frame(width: geo.size.width * percentage, height: geo.size.height * percentage, alignment: .center)
}
}
}
struct GrannySquareComplete_Previews: PreviewProvider {
static var previews: some View {
GrannySquareComplete()
}
}

Related

Swift UI exporting content of canvas

I have a canvas where the user can draw stuff on it. I want to export whatever the user draw on my canvas and I'm using the following extension to get images out of Views
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
But unfortunatelly this extension works fine for buttons on any simple view. Whenever I use it for my canvas, it gives me a blank image, it appears it doesn't get the content of my canvas.
Is it possible to export my canvas content?
Is it because I'm drawing my canvas with a stroke?
This is how I'm using my canvas:
#State private var currentLine = Line(color: .red)
#State private var drawedLines = [Line]()
#State private var selectedColor: Color = .red
#State private var selectedSize = 1.0
private var colors:[Color] = [.red, .green, .blue, .black, .orange, .yellow, .brown, .pink, .purple, .indigo, .cyan, .mint]
var canvasPallete: some View {
return Canvas { context, size in
for line in drawedLines {
var path = Path()
path.addLines(line.points)
context.stroke(path, with: .color(line.color), lineWidth: line.size)
}
}.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged({ value in
let point = value.location
currentLine.points.append(point)
currentLine.color = selectedColor
currentLine.size = selectedSize
drawedLines.append(currentLine)
})
.onEnded({ value in
currentLine = Line(color: selectedColor)
})
)
}
var body: some View {
VStack {
canvasPallete
.frame(width: 300, height: 300)
Divider()
HStack(alignment: .bottom ) {
VStack{
Button {
print("Will save draw image")
let image = canvasPallete.snapshot()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
} label: {
Text("Save")
}
.padding([.bottom], 25)
Button {
drawedLines.removeAll()
} label: {
Image(systemName: "trash.circle.fill")
.font(.system(size: 40))
.foregroundColor(.red)
}.frame(width: 70, height: 50, alignment: .center)
}
VStack(alignment: .leading) {
Text("Colors:")
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(colors, id:\.self){color in
ColoredCircleButton(color: color, selected: color == selectedColor) {
selectedColor = color
}
}
}
.padding(.all, 5)
}
Text("Size:")
Slider(value: $selectedSize, in: 0.2...8)
.padding([.leading,.trailing], 20)
.tint(selectedColor)
}
.padding([.bottom], 10)
}
}.frame(minWidth: 400, minHeight: 400)
}
}
struct ColoredCircleButton: View {
let color: Color
var selected = false
let onTap: (() -> Void)
var body: some View {
Circle()
.frame(width: 25, height: 25)
.foregroundColor(color)
.overlay(content: {
if selected {
Circle().stroke(.gray, lineWidth: 3)
}
})
.onTapGesture {
onTap()
}
}
}
This works fine!
The issue you are encountering is that in your sample code, canvasPallete didn't get a frame modifier when snapshot() was called on it. So it got rendered at a size that's not what you want, probably (0,0).
If you change the snapshot line it will work:
let image = canvasPallete
.frame(width: 300, height: 300)
.snapshot()
One more thing! In iOS 16, there's new API for rendering a view to an Image. So for iOS 16 and up, you can skip the View extension and write:
let renderer = ImageRenderer(content: canvasPallete)
if let image = renderer.uiImage {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}

Increase/Decrease the size of a view horizontally by dragging the edges of it

I've seen a few similar examples of this such as How to correctly do up an adjustable split view in SwiftUI? and How to resize UIView by dragging from its edges? but I can't find exactly what I'm looking for that correlates correctly across to SwiftUI
I have a view that I want the user to be able to adjust the width of via a 'grab bar' on the right of the view. When the user drags this bar left (decreases view width) and to the right (increases the view width). How can I go about doing this?
In the example, RedRectangle is my view that i'm trying to adjust which comprises of a Rectangle and the Resizer which is manipulated to adjust the size. What am I doing wrong here?
Additionally, there isn't a gradual animation/transition of the frame being increased/decreased and it just seems to jump. How can I achieve this?
Reproducible example linked here:
import SwiftUI
struct ContentView: View {
#State var resizedWidth: CGFloat?
var body: some View {
HStack(alignment: .center) {
Spacer()
RedRectangle(width: 175, resizedWidth: resizedWidth)
Resizer()
.gesture(
DragGesture()
.onChanged({ value in
resizedWidth = max(80, resizedWidth ?? 0 + value.translation.width)
})
)
Spacer()
}
}
}
struct RedRectangle: View {
let width: CGFloat
var resizedWidth: CGFloat?
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: resizedWidth != nil ? resizedWidth : width, height: 300)
.frame(minWidth: 80, maxWidth: 400)
}
}
struct Resizer: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 8, height: 75)
.cornerRadius(10)
}
}
import SwiftUI
struct ContentView: View {
let minWidth: CGFloat = 100
#State var width: CGFloat?
var body: some View {
HStack(alignment: .center) {
Spacer()
RedRectangle(width: width ?? minWidth)
Resizer()
.gesture(
DragGesture()
.onChanged { value in
width = max(minWidth, width! + value.translation.width)
}
)
Spacer()
}
.onAppear {
width = minWidth
}
}
}
struct RedRectangle: View {
let width: CGFloat
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: width, height: 100)
}
}
struct Resizer: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 8, height: 75)
.cornerRadius(10)
}
}

SwiftUI - Button inside a custom Collection view is not tappable completely

I have a set of buttons to show for the user and I used CollectionView to align the buttons. Each button is a Vstack with an Image and Text components. The tap is reactive only on the image but not on Text and the padding space around.
I am looking to solve this to make it reactive all over the button.
I found suggestions
to set ContentShape to rectangle and it didn't work
use Hstack to insert spaces on Left and right of the Text but that didn't work either.
Sample code:
ToolBarItem:
var body: some View {
VStack {
Button(action: {
// Delegate event to caller/parent view
self.onClickAction(self.toolBarItem)
}) {
VStack {
HStack {
Spacer()
Image(self.toolBarItem.selectedBackgroundImage)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.padding(EdgeInsets(top: 5, leading: 3, bottom: 0, trailing: 3))
.frame(width: CGFloat(self.toolBarItem.cellWidth * 0.60),
height: CGFloat(self.toolBarItem.cellHeight * 0.60))
Spacer()
}
.contentShape(Rectangle())
HStack {
Spacer()
Text(self.toolBarMenuInfo.specialSelectedName)
.foregroundColor(Color.red)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 5, trailing: 0))
Spacer()
}
.contentShape(Rectangle())
}
.frame(width: CGFloat(self.toolBarItem.cellWidth),
height: CGFloat(self.toolBarItem.cellHeight))
.background(Color.blue.opacity(0.5))
}
}
}
The above ToolBarItem is placed inside the Collection view (custom Object created by me) for as many items required. Refer attachment and the tap occurs only on the image surrounded by green marking.
has anyone had similar issue? Any inputs is appreciated.
I strongly suspect that you issue has to do with the border. but I don't know exactly because you haven't provided that code.
Here is a version of the button view that would give you the effect you see to want.
struct FloatingToolbarButtonView: View {
#Binding var model: ButtonModel
let size: CGSize
var body: some View {
Button(action: {
//Set the model's variable to selected
model.isSelected.toggle()
//Perform the action
model.onClick()
}, label: {
VStack {
//REMOVE systemName: in your code
Image(systemName: model.imageName)
//.renderingMode(.original)
.resizable()
//Maintains proportions
.scaledToFit()
//Set Image color
.foregroundColor(.white)
//Works with most images to change color
.colorMultiply(model.colorSettings.imageNormal)
.padding(5)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
//Set border color/width
.border(Color.green, width: model.isSelected ? 3:0)
Spacer()
Text(model.label)
//Set Text color
.foregroundColor(model.colorSettings.labelNormal)
}
.padding(EdgeInsets(top: 5, leading: 3, bottom: 0, trailing: 3))
}).frame(width: size.width, height: size.height, alignment: .center)
.background(model.colorSettings.backgroundNormal)
}
}
And this is what the model I used looks like
//Holds Button information
struct ButtonModel: Identifiable{
let id: UUID = UUID()
var label: String
var imageName: String
///Action to be called when the button is pressed
var onClick: () -> Void
///identify if the user has selected this button
var isSelected: Bool = false
var colorSettings: ButtonColorSettings
}
I created the buttons in a view model so I can have an easy to to set the action and access isSelected as needed.
//ViewModel that deals with all the button creation and onClick actions
class FloatingToolbarParentViewModel: ObservableObject{
//Settings the buttons at this level lets you read `isPressed`
#Published var horizontalButtons: [ButtonModel] = []
#Published var moreButton: [ButtonModel] = []
#Published var verticalButtons: [ButtonModel] = []
init(){
horizontalButtons = horizontalSamples
moreButton = [mer]
verticalButtons = veticalSamples
}
}
//MARK: Buttons
extension FloatingToolbarParentViewModel{
//MARK: SAMPLES fill in with your data
var identify:ButtonModel {ButtonModel(label: "Identify", imageName: "arrow.up.backward", onClick: {print(#function + " Identfy")}, colorSettings: .white)}
var tiltak:ButtonModel {ButtonModel(label: "Tiltak", imageName: "scissors", onClick: {print(#function + " Tiltak")}, colorSettings: .white)}
var tegn:ButtonModel { ButtonModel(label: "Tegn", imageName: "pencil", onClick: {print(#function + " Tegn")}, colorSettings: .white)}
var bestand:ButtonModel {ButtonModel(label: "Bestand", imageName: "leaf", onClick: {print(#function + " Identfy")}, colorSettings: .red)}
var mer:ButtonModel {ButtonModel(label: "Mer", imageName: "ellipsis.circle", onClick: {print(#function + " Mer")}, colorSettings: .red)}
var kart:ButtonModel {ButtonModel(label: "Kart", imageName: "map.fill", onClick: {print(#function + " Kart")}, colorSettings: .white)}
var posisjon:ButtonModel {ButtonModel(label: "Posisjon", imageName: "magnifyingglass", onClick: {print(#function + " Posisjon")}, colorSettings: .white)}
var spor:ButtonModel {ButtonModel(label: "Spor", imageName: "circle.fill", onClick: {print(#function + " Spor")}, colorSettings: .red)}
var horizontalSamples :[ButtonModel] {[identify,tiltak,tegn,bestand]}
var veticalSamples :[ButtonModel] {[kart,posisjon,spor]}
}
The rest of the code to get the sample going is below. It isn't really needed but it will give you a working product
struct FloatingToolbarParentView: View {
#State var region: MKCoordinateRegion = .init()
#StateObject var vm: FloatingToolbarParentViewModel = .init()
var body: some View {
ZStack{
Map(coordinateRegion: $region)
ToolbarOverlayView( horizontalButtons: $vm.horizontalButtons, cornerButton: $vm.moreButton, verticalButtons: $vm.verticalButtons)
}
}
}
struct ToolbarOverlayView: View{
#State var buttonSize: CGSize = .zero
#Binding var horizontalButtons: [ButtonModel]
#Binding var cornerButton: [ButtonModel]
#Binding var verticalButtons: [ButtonModel]
var body: some View{
GeometryReader{ geo in
VStack{
HStack{
Spacer()
VStack{
Spacer()
FloatingToolbarView(buttons: $verticalButtons, buttonSize: buttonSize, direction: .vertical)
}
}
Spacer()
HStack{
Spacer()
FloatingToolbarView(buttons: $horizontalButtons, buttonSize: buttonSize)
FloatingToolbarView(buttons: $cornerButton, buttonSize: buttonSize)
}
//Adjust the button size on appear and when the orientation changes
.onAppear(perform: {
setButtonSize(size: geo.size)
})
.onChange(of: geo.size.width, perform: { new in
setButtonSize(size: geo.size)
})
}
}
}
//Sets the button size using and minimum and maximum values accordingly
//landscape and portrait have oppositive min and max
func setButtonSize(size: CGSize){
buttonSize = CGSize(width: min(size.width, size.height) * 0.15, height: max(size.width, size.height) * 0.1)
}
}
//Toolbar group for an array of butons
struct FloatingToolbarView: View {
#Binding var buttons :[ButtonModel]
let buttonSize: CGSize
var direction: Direction = .horizontal
var body: some View {
Group{
switch direction {
case .horizontal:
HStack(spacing: 0){
ForEach($buttons){$button in
FloatingToolbarButtonView(model: $button, size: buttonSize)
}
}
case .vertical:
VStack(spacing: 0){
ForEach($buttons){$button in
FloatingToolbarButtonView(model: $button, size: buttonSize)
}
}
}
}
}
enum Direction{
case horizontal
case vertical
}
}
#available(iOS 15.0, *)
struct FloatingToolbarParentView_Previews: PreviewProvider {
static var previews: some View {
FloatingToolbarParentView()
FloatingToolbarParentView().previewInterfaceOrientation(.landscapeLeft)
}
}
//Holds Button Color information
//You havent provided much info on this so I assume that you are setting the colors somewhere
struct ButtonColorSettings{
var labelNormal: Color
var imageNormal: Color
var backgroundNormal: Color
//Sample Color configuration per image
static var white = ButtonColorSettings(labelNormal: .white, imageNormal: .white, backgroundNormal: .black.opacity(0.5))
static var red = ButtonColorSettings(labelNormal: .black, imageNormal: .red, backgroundNormal: .white)
}
Have you tried putting .contentShape(Rectangle()) on the whole VStack inside the Button or on the button itself? That should probably solve it.

How to consistently separate elements on a HStack

I have a set of elements on a HStack that are composed of a circle and some label text underneath it. These elements have a separation between them.
Everything works as expected until one of the labels is bigger than the circle. Then, the text increases the actual width of the element, making the visible separation between the circles, off.
Is there any way to evenly separate the circles, ignoring the text?
struct Item: View {
let color: Color
let label: String
var body: some View {
VStack {
Circle()
.fill(color)
.frame(width: 40, height: 40)
Text(label)
}
}
}
struct ItemSeparation: View {
var body: some View {
HStack(alignment: .top, spacing: 30) {
Item(color: .yellow, label: "Berlin")
Item(color: .green, label: "Copenhagen")
Item(color: .blue, label: "Madrid")
Item(color: .purple, label: "Helsinki")
}
}
}
These two solutions have different compromises. You'll need to decide what happens, for example, if the views will be too wide for the parent view based on the longest label. Option 1 wraps the text. Option 2 makes the view expand beyond its parent.
Option #1:
Use LazyVGrid:
struct ContentView: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
]
var body: some View {
LazyVGrid(columns: columns, spacing: 30) {
Item(color: .yellow, label: "Berlin")
Item(color: .green, label: "Copenhagen")
Item(color: .blue, label: "Madrid")
Item(color: .purple, label: "Helsinki")
}
}
}
Option #2:
Use a PreferenceKey and a GeometryReader to get the size of the widest View and propagate the changes back up to the parent.
struct Item: View {
let color: Color
let label: String
let width: CGFloat
var body: some View {
VStack {
Circle()
.fill(color)
.frame(width: 40, height: 40)
Text(label)
}
.border(Color.blue)
.background(
GeometryReader {
Color.clear.preference(key: ViewWidthKey.self,
value: $0.size.width)
}.scaledToFill()
)
.frame(width: width)
}
}
struct ViewWidthKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
let nextValue = nextValue()
guard nextValue > value else { return }
value = nextValue
}
}
struct ContentView: View {
#State private var maxWidth : CGFloat = 0
var body: some View {
HStack(alignment: .top, spacing: 30) {
Item(color: .yellow, label: "Berlin", width: maxWidth)
Item(color: .green, label: "Copenhagen", width: maxWidth)
Item(color: .blue, label: "Madrid", width: maxWidth)
Item(color: .purple, label: "Helsinki", width: maxWidth)
}.onPreferenceChange(ViewWidthKey.self) { width in
self.maxWidth = width
print(width)
}
}
}

SwiftUI - Position a rectangle relative to another view in ZStack

The task
I am creating a bar chart with SwiftUI and I am looking for the best approach to position the average indicator, the horizontal yellow line, shown in the picture below:
The goal
I need to be able to position the yellow line (rectangle with 1 point in height) relative to the bars (also rectangles) of the chart. For simplicity let us imagine that the tallest possible bar is 100 points and the average is 75. My goal is to draw the yellow line exactly 75 points above the bottom of the bars.
The problem
In my example I am placing the line on top of the chart using a ZStack. I can position the line vertically using a GeometryReader, however I do not know how to position it in reference to the bottom of the bars.
Here my playground code:
import SwiftUI
import PlaygroundSupport
struct BarChart: View {
var measurements : [Int]
var pastColor: Color
var todayColor: Color
var futureColor: Color
let labelsColor = Color(UIColor(white: 0.5, alpha: 1.0))
func monthAbbreviationFromInt(_ day: Int) -> String {
let da = Calendar.current.shortWeekdaySymbols
return da[day]
}
var body: some View {
ZStack {
VStack {
HStack {
Rectangle()
.fill(Color.yellow)
.frame(width: 12, height: 2, alignment: .center)
Text("Desired level")
.font(.custom("Helvetica", size: 10.0))
.foregroundColor(labelsColor)
}
.padding(.bottom , 50.0)
HStack {
ForEach(0..<7) { day in
VStack {
Spacer()
Text(String(self.measurements[day]))
.frame(width: CGFloat(40), height: CGFloat(20))
.font(.footnote)
.lineLimit(1)
.minimumScaleFactor(0.5)
.foregroundColor(self.labelsColor)
Rectangle()
.fill(day > 0 ? self.futureColor : self.pastColor)
.frame(width: 20, height: CGFloat(self.measurements[day]*2))
.transition(.slide)
.cornerRadius(3.0)
Text("\(self.monthAbbreviationFromInt(day))\n\(day)")
.multilineTextAlignment(.center)
.font(.footnote)
.frame(height: 20)
.lineLimit(2)
.minimumScaleFactor(0.2)
.foregroundColor(self.labelsColor)
}
.frame(height: 200, alignment: .bottom )
}
}
}
.padding()
VStack {
GeometryReader { geometry in
//Yellow line
Rectangle()
.path(in: CGRect(x: 0,
y: 350, //This is now a random value, but I should detect and use the bottom coordinate of the bars to place the line at exactly 75 points above that coordinate
width: geometry.size.width,
height: 1))
.fill(Color.yellow)
}
}
}
}
}
How can I get the relative position of the bottom of the bars so that I can place the line correctly?
Is there another approach you would suggest?
Thank you in advance
example
import SwiftUI
struct Data: Identifiable {
let id = UUID()
let value: CGFloat
let color: Color
}
struct ContentView: View {
let data = [Data(value: 35, color: .red),
Data(value: 100, color: .green),
Data(value: 70, color: .yellow),
Data(value: 25, color: .blue),
Data(value: 55, color: .orange)]
var maxValue: CGFloat {
data.map { (element) -> CGFloat in
element.value
}.max() ?? 1.0
}
var average: CGFloat {
guard !data.isEmpty else { return 0 }
let sum = data.reduce(into: CGFloat.zero) { (res, data) in
res += data.value
}
return sum / CGFloat(data.count)
}
var body: some View {
HStack {
ForEach(data) { (bar) in
Bar(t: bar.value / self.maxValue, color: bar.color, width: 20)
}
}
.background(Color.pink.opacity(0.1))
.overlay(
GeometryReader { proxy in
Color.primary.frame(height: 1).position(x: proxy.size.width / 2, y: proxy.size.height * ( 1 - self.average / self.maxValue))
}
).padding(.vertical, 100).padding(.horizontal, 20)
}
}
struct Bar: View {
let t: CGFloat
let color: Color
let width: CGFloat
var body: some View {
GeometryReader { proxy in
Path { (path) in
path.move(to: .init(x: proxy.size.width / 2, y: proxy.size.height))
path.addLine(to: .init(x: proxy.size.width / 2, y: 0))
}
.trim(from: 0, to: self.t)
.stroke(lineWidth: self.width)
.foregroundColor(self.color)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and result
or slightly adopted to show labels