I am drawing a line with Path, and on that line I want to have a circle if I play around I can get the circle on the line of course. However I do not understand why this code does not put the circle on the line:
struct CircleOnLineView: View {
func createCirle() -> some View {
return Circle().fill(Color.blue)
}
var body: some View {
GeometryReader { geometry in
ZStack {
Path { path in
path.move(to: CGPoint(x: 0, y: geometry.size.height / 2))
path.addLine(to: CGPoint(x: geometry.size.width, y: geometry.size.height / 2))
}
.stroke(Color.gray, lineWidth: 3)
Circle()
.fill(Color.blue)
.position(CGPoint(x: 0 , y: geometry.size.height / 2))
.frame(width: 5, height: 5)
}
}
}
}
Order of modifiers in this case is important. Here is how it is expected (1st - made size of shape, 2nd - position it):
Circle()
.fill(Color.blue)
.frame(width: 5, height: 5)
.position(CGPoint(x: 0 , y: geometry.size.height / 2))
Related
I'm trying to create a growing TextEditor as input for a chat view.
The goal is to have a box which expands until 6 lines are reached for example. After that it should be scrollable.
I already managed to do this with strings, which contain line breaks \n.
TextEditor(text: $composedMessage)
.onChange(of: self.composedMessage, perform: { value in
withAnimation(.easeInOut(duration: 0.1), {
if (value.numberOfLines() < 6) {
height = startHeight + CGFloat((value.numberOfLines() * 20))
}
if value.numberOfLines() == 0 || value.isEmpty {
height = 50
}
})
})
I created a string extension which returns the number of line breaks by calling string.numberOfLines() var startHeight: CGFloat = 50
The problem: If I paste a text which contains a really long text, it's not expanding when this string has no line breaks. The text get's broken in the TextEditor.
How can I count the number of breaks the TextEditor makes and put a new line character at that position?
Here's a solution adapted from question and answer,
struct ChatView: View {
#State var text: String = ""
// initial height
#State var height: CGFloat = 30
var body: some View {
ZStack(alignment: .topLeading) {
Text("Placeholder")
.foregroundColor(.appLightGray)
.font(Font.custom(CustomFont.sofiaProMedium, size: 13.5))
.padding(.horizontal, 4)
.padding(.vertical, 9)
.opacity(text.isEmpty ? 1 : 0)
TextEditor(text: $text)
.foregroundColor(.appBlack)
.font(Font.custom(CustomFont.sofiaProMedium, size: 14))
.frame(height: height)
.opacity(text.isEmpty ? 0.25 : 1)
.onChange(of: self.text, perform: { value in
withAnimation(.easeInOut(duration: 0.1), {
if (value.numberOfLines() < 6) {
// new height
height = 120
}
if value.numberOfLines() == 0 || value.isEmpty {
// initial height
height = 30
}
})
})
}
.padding(4)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.appLightGray, lineWidth: 0.5)
)
}
}
And the extension,
extension String {
func numberOfLines() -> Int {
return self.numberOfOccurrencesOf(string: "\n") + 1
}
func numberOfOccurrencesOf(string: String) -> Int {
return self.components(separatedBy:string).count - 1
}
}
I found a solution!
For everyone else trying to solve this:
I added a Text with the same width of the input field and then used a GeometryReader to calculate the height of the Text which automatically wraps. Then if you divide the height by the font size you get the number of lines.
You can make the text field hidden (tested in iOS 14 and iOS 15 beta 3)
If you're looking for an iOS 15 solution, I spent a while and figured it out. I didn't want to have to resort to UIKit or ZStacks with overlays or duplicative content as a "hack". I wanted it to be pure SwiftUI.
I ended up creating a separate struct that I could reuse anywhere I needed it, as well as add additional parameters in my various views.
Here's the struct:
struct FieldMultiEntryTextDynamic: View {
var text: Binding<String>
var body: some View {
TextEditor(text: text)
.padding(.vertical, -8)
.padding(.horizontal, -4)
.frame(minHeight: 0, maxHeight: 300)
.font(.custom("HelveticaNeue", size: 17, relativeTo: .headline))
.foregroundColor(.primary)
.dynamicTypeSize(.medium ... .xxLarge)
.fixedSize(horizontal: false, vertical: true)
} // End Var Body
} // End Struct
The cool thing about this is that you can have placeholder text via an if statement and it supports dynamic type sizes.
You can implement it as follows:
struct MyView: View {
#FocusState private var isFocused: Bool
#State private var myName: String = ""
var body: some View {
HStack(alignment: .top) {
Text("Name:")
ZStack(alignment: .trailing) {
if myName.isEmpty && !isFocused {
Text("Type Your Name")
.font(.custom("HelveticaNeue", size: 17, relativeTo: .headline))
.foregroundColor(.secondary)
}
HStack {
VStack(alignment: .trailing, spacing: 5) {
FieldMultiEntryTextDynamic(text: $myName)
.multilineTextAlignment(.trailing)
.keyboardType(.alphabet)
.focused($isFocused)
}
}
}
}
.padding()
.background(.blue)
}
}
Hope it helps!
I'm using the glify plugin for Leaflet, and can't for the life of me figure out how to set the color of my points to a function.
This works fine:
L.glify.points({
data: data,
map: map,
opacity: 1,
size: 10,
color: 'red',
However this returns all black points:
L.glify.points({
data: data,
map: map,
opacity: 1,
size: 10,
color: function(){
if ( 1 > 0 ){ return 'red';}else{return 'blue';}
},
Does anyone have any idea what I need to do here?
figured it out, needs to be formatted like this:
color: function(){
if (1 > 0){
return {
r: 0,
g: .51,
b: .1
};
}else{
return {
r: 30,
g: 1,
b: 2
};
}
},
You need to convert your hex color code into rgb form using below function
function fromHex(hex) {
if (hex.length < 6) return null;
hex = hex.toLowerCase();
if (hex[0] === '#') {
hex = hex.substring(1, hex.length);
}
var r = parseInt(hex[0] + hex[1], 16),
g = parseInt(hex[2] + hex[3], 16),
b = parseInt(hex[4] + hex[5], 16);
return {
r: r / 255,
g: g / 255,
b: b / 255
};
}
and then return it like this :
L.glify.points({
data: data,
map: map,
opacity: 1,
size: 10,
color: function(){
if ( 1 > 0 ){
return fromHex("#FF0000");
}else{
return fromHex("#0000FF");
}
}
});
I recently switched from using PaintCode 2 to PaintCode 3, I am using it together with xCode/Swift.
I noticed however, that all my image generating functions not behave differently. They seam to standard addopt cap insets.
As an example below you can find one canvas "ViewMissingImage", and how its configured in PaintCode (2 or 3 its identical).
Code generated via PaintCode 2
public class func imageOfViewMissingImage(frame frame: CGRect = CGRect(x: 6, y: 5, width: 109, height: 109)) -> UIImage {
UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
PaintCode.drawViewMissingImage(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
let imageOfViewMissingImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return imageOfViewMissingImage
}
Code generated via PaintCode 3
public dynamic class func imageOfViewMissingImage(imageSize imageSize: CGSize = CGSize(width: 109, height: 109)) -> UIImage {
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
PaintCode.drawViewMissingImage(frame: CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height))
let imageOfViewMissingImage = UIGraphicsGetImageFromCurrentImageContext()!.resizableImageWithCapInsets(UIEdgeInsetsZero, resizingMode: .Tile)
UIGraphicsEndImageContext()
return imageOfViewMissingImage
}
I think that the PaintCode 2 never used the capp insets, maybe it was a bug?
I do not want these cap insets, how can I get rid of them?
The solution is straightforward:
Put the Cap Inset on "Stretch" instead of tile in PaintCode UI!
I have this code for drawing a rotated icon on a google map:
function createMarker(device) {
var wtf = new google.maps.Marker({
map: window.map_,
position: new google.maps.LatLng(device.lat, device.lng),
icon: {
path: 'M350,0 700,700 350,550 0,700',
fillColor: "limegreen",
fillOpacity: 0.8,
scale: 0.04,
strokeColor: 'limegreen',
strokeWeight: 1,
anchor: new google.maps.Point(800, 800),
rotation: device.head,
}
});
return wtf;
}
The problem is the rotation rotates on one of the corners of the SVG - not the center. I found the svg path somewhere, scaled it by trial and error, and guessed at the anchor. When I add a label, instead of the label being under the icon it is all messed up. The label uses the same lat/long as the marker.
See example:
I'm finding it impossible to get the icon to rotate "on the spot" above the label. Any ideas on how to get this to work? Thanks
This isn't ideal but it works okay. This is TypeScript code. You can't go back to JavaScript after writing in TypeScript, it is just too painful.
Given that pos is a LatLng:
let pos = new this.google.maps.LatLng(lat, lng);
Methods:
public createMarker(heading: number, pos: any) {
let marky = new this.google.maps.Marker({
position: pos,
map: this.google_map,
icon: {
path: this.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
fillOpacity: 1,
fillColor: 'orange',
strokeWeight: 1.5,
scale: 2,
strokeColor: 'darkblue',
rotation: heading,
anchor: this.getRotation(heading),
},
});
return marky;
}
public createLabel(name: string, pos: any) {
let label = new MapLabel({
text: name,
position: pos,
map: this.google_map,
fontSize: 17,
fontColor: 'darkred',
align: 'center'
});
return label;
}
public getRotation(angle: number) {
if (angle <= 30) return new this.google.maps.Point(0, 5);
if (angle <= 45) return new this.google.maps.Point(0, 5);
if (angle <= 60) return new this.google.maps.Point(2, 4);
if (angle <= 120) return new this.google.maps.Point(3, 0);
if (angle <= 150) return new this.google.maps.Point(0, 0);
if (angle <= 210) return new this.google.maps.Point(0, 0);
if (angle <= 220) return new this.google.maps.Point(-1, 0);
if (angle <= 240) return new this.google.maps.Point(-3, 0);
if (angle <= 280) return new this.google.maps.Point(-3, -3);
return new this.google.maps.Point(0, 5);
}
I have an algorithm for Floodfilling a canvas. Im trying to incorporate this with fabricJS. So here is the dilemna.... I create a fabric.Canvas(). Which creates a wrapper canvas and also an upper-canvas canvas. I click on the canvas to apply my Floodfill(). This works fine and applies my color. But as soon as i go to drag my canvas objects around, or add additional objects to the canvas, the color disappears and looks like it resets of sort.
Any idea why this is?
This happen because fabricjs wipe out all canvas every frame and redraw from its internal data.
I made a JSfiddle that implements Flood Fill for Fabric JS. Check it here: https://jsfiddle.net/av01d/dfvp9j2u/
/*
* FloodFill for fabric.js
* #author Arjan Haverkamp (av01d)
* #date October 2018
*/
var FloodFill = {
// Compare subsection of array1's values to array2's values, with an optional tolerance
withinTolerance: function(array1, offset, array2, tolerance)
{
var length = array2.length,
start = offset + length;
tolerance = tolerance || 0;
// Iterate (in reverse) the items being compared in each array, checking their values are
// within tolerance of each other
while(start-- && length--) {
if(Math.abs(array1[start] - array2[length]) > tolerance) {
return false;
}
}
return true;
},
// The actual flood fill implementation
fill: function(imageData, getPointOffsetFn, point, color, target, tolerance, width, height)
{
var directions = [[1, 0], [0, 1], [0, -1], [-1, 0]],
coords = [],
points = [point],
seen = {},
key,
x,
y,
offset,
i,
x2,
y2,
minX = -1,
maxX = -1,
minY = -1,
maxY = -1;
// Keep going while we have points to walk
while (!!(point = points.pop())) {
x = point.x;
y = point.y;
offset = getPointOffsetFn(x, y);
// Move to next point if this pixel isn't within tolerance of the color being filled
if (!FloodFill.withinTolerance(imageData, offset, target, tolerance)) {
continue;
}
if (x > maxX) { maxX = x; }
if (y > maxY) { maxY = y; }
if (x < minX || minX == -1) { minX = x; }
if (y < minY || minY == -1) { minY = y; }
// Update the pixel to the fill color and add neighbours onto stack to traverse
// the fill area
i = directions.length;
while (i--) {
// Use the same loop for setting RGBA as for checking the neighbouring pixels
if (i < 4) {
imageData[offset + i] = color[i];
coords[offset+i] = color[i];
}
// Get the new coordinate by adjusting x and y based on current step
x2 = x + directions[i][0];
y2 = y + directions[i][1];
key = x2 + ',' + y2;
// If new coordinate is out of bounds, or we've already added it, then skip to
// trying the next neighbour without adding this one
if (x2 < 0 || y2 < 0 || x2 >= width || y2 >= height || seen[key]) {
continue;
}
// Push neighbour onto points array to be processed, and tag as seen
points.push({ x: x2, y: y2 });
seen[key] = true;
}
}
return {
x: minX,
y: minY,
width: maxX-minX,
height: maxY-minY,
coords: coords
}
}
}; // End FloodFill
var fcanvas; // Fabric Canvas
var fillColor = '#f00';
var fillTolerance = 2;
function hexToRgb(hex, opacity) {
opacity = Math.round(opacity * 255) || 255;
hex = hex.replace('#', '');
var rgb = [], re = new RegExp('(.{' + hex.length/3 + '})', 'g');
hex.match(re).map(function(l) {
rgb.push(parseInt(hex.length % 2 ? l+l : l, 16));
});
return rgb.concat(opacity);
}
function floodFill(enable) {
if (!enable) {
fcanvas.off('mouse:down');
fcanvas.selection = true;
fcanvas.forEachObject(function(object){
object.selectable = true;
});
return;
}
fcanvas.deactivateAll().renderAll(); // Hide object handles!
fcanvas.selection = false;
fcanvas.forEachObject(function(object){
object.selectable = false;
});
fcanvas.on({
'mouse:down': function(e) {
var mouse = fcanvas.getPointer(e.e),
mouseX = Math.round(mouse.x), mouseY = Math.round(mouse.y),
canvas = fcanvas.lowerCanvasEl,
context = canvas.getContext('2d'),
parsedColor = hexToRgb(fillColor),
imageData = context.getImageData(0, 0, canvas.width, canvas.height),
getPointOffset = function(x,y) {
return 4 * (y * imageData.width + x)
},
targetOffset = getPointOffset(mouseX, mouseY),
target = imageData.data.slice(targetOffset, targetOffset + 4);
if (FloodFill.withinTolerance(target, 0, parsedColor, fillTolerance)) {
// Trying to fill something which is (essentially) the fill color
console.log('Ignore... same color')
return;
}
// Perform flood fill
var data = FloodFill.fill(
imageData.data,
getPointOffset,
{ x: mouseX, y: mouseY },
parsedColor,
target,
fillTolerance,
imageData.width,
imageData.height
);
if (0 == data.width || 0 == data.height) {
return;
}
var tmpCanvas = document.createElement('canvas'), tmpCtx = tmpCanvas.getContext('2d');
tmpCanvas.width = canvas.width;
tmpCanvas.height = canvas.height;
var palette = tmpCtx.getImageData(0, 0, tmpCanvas.width, tmpCanvas.height); // x, y, w, h
palette.data.set(new Uint8ClampedArray(data.coords)); // Assuming values 0..255, RGBA
tmpCtx.putImageData(palette, 0, 0); // Repost the data.
var imgData = tmpCtx.getImageData(data.x, data.y, data.width, data.height); // Get cropped image
tmpCanvas.width = data.width;
tmpCanvas.height = data.height;
tmpCtx.putImageData(imgData,0,0);
fcanvas.add(new fabric.Image(tmpCanvas, {
left: data.x,
top: data.y,
selectable: false
}))
}
});
}
$(function() {
// Init Fabric Canvas:
fcanvas = new fabric.Canvas('c', {
backgroundColor:'#fff',
enableRetinaScaling: false
});
// Add some demo-shapes:
fcanvas.add(new fabric.Circle({
radius: 80,
fill: false,
left: 100,
top: 100,
stroke: '#000',
strokeWidth: 2
}));
fcanvas.add(new fabric.Triangle({
width: 120,
height: 160,
left: 50,
top: 50,
stroke: '#000',
fill: '#00f',
strokeWidth: 2
}));
fcanvas.add(new fabric.Rect({
width: 120,
height: 160,
left: 150,
top: 50,
fill: 'red',
stroke: '#000',
strokeWidth: 2
}));
fcanvas.add(new fabric.Rect({
width: 200,
height: 120,
left: 200,
top: 120,
fill: 'green',
stroke: '#000',
strokeWidth: 2
}));
/* Images work very well too. Make sure they're CORS
enabled though! */
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() {
fcanvas.add(new fabric.Image(img, {
left: 300,
top: 100,
angle: 30,
}));
}
img.src = 'http://misc.avoid.org/chip.png';
});