Рисуем линии, дуги, диаграммы
В этом разделе вы узнаете, как рисовать линии, дуги и диаграммы с помощью Path и встроенных форм, таких как Circle и RoundedRectangle, в SwiftUI. Вот что мы изучим:
* Понимание Path и рисование линий
* Что такое протокол Shape и как нарисовать настраиваемую форму путем соответствия протоколу
* Рисование диаграммы
* Создание индикатора прогресса с помощью открытого круга
* Рисование «pie chart» диаграммы
Мы собираемся нарисовать множество различных фигур и таким образом научимся использовать ключевую в этом вопросе структуру Path и протокол Shape.
Понимание Path
В SwiftUI вы рисуете линии и формы с помощью Path. Если обратиться к документации Apple, Path - это структура, содержащая контур 2D-формы. Основная идея заключается в установке начальной точки, а затем рисовании линий от точки к точке. Давайте рассмотрим пример. Возьмем фигуру 2 и пошагово рассмотрим, как рисуется этот прямоугольник.
Если бы вы хотели объяснить мне, как нарисовать прямоугольник шаг за шагом, вы, вероятно, предоставили бы следующее описание:
Переместить точку на позицию (20, 20)
Нарисовать линию от (20, 20) до (300, 20)
Нарисовать линию от (300, 20) до (300, 200)
Нарисовать линию от (300, 200) до (20, 200)
Заполнить всю область зеленым цветом
Именно так работает Path! Давайте запишем ваше описание в коде:
import SwiftUI
struct ContentView: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 300, y: 20))
path.addLine(to: CGPoint(x: 300, y: 200))
path.addLine(to: CGPoint(x: 20, y: 200))
path.closeSubpath()
}
.fill(Color.green)
}
}
В приведенном выше коде мы использовали Path для рисования прямоугольника и заполнили его зеленым цветом. Мы переместились в начальную точку (20, 20), нарисовали линии от точки к точке, чтобы создать форму прямоугольника, и закрыли контур с помощью path.closeSubpath(). Затем мы использовали модификатор .fill, чтобы задать зеленый цвет для заполнения области. Это простой пример использования Path для рисования векторных изображений в SwiftUI. Вы можете использовать Path для создания более сложных форм и заполнения их любым цветом или градиентом.
Используем Stroke чтобы отрисовать границы (Borders)
В SwiftUI вы можете использовать Path для рисования линий, а не только заполнять области цветом. Для этого вы можете использовать модификатор .stroke, который позволяет установить ширину и цвет линии. Например, вы можете нарисовать контур прямоугольника, используя Path, и установить ширину и цвет линии с помощью модификатора .stroke.
В приведенном выше коде мы использовали Path для рисования прямоугольника и заполнили его зеленым цветом. Если вы хотите просто нарисовать линии, вы можете удалить модификатор .fill и добавить модификатор .stroke, указав ширину и цвет линии. Например:
struct ContentView: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 300, y: 20))
path.addLine(to: CGPoint(x: 300, y: 200))
path.addLine(to: CGPoint(x: 20, y: 200))
path.closeSubpath()
}
.stroke(Color.green, lineWidth: 2)
}
}
Рисуем кривые линии
В SwiftUI вы можете использовать Path для рисования сложных форм, таких как кривые и дуги. Для этого вы можете использовать методы addQuadCurve, addCurve и addArc, которые позволяют создавать кривые и дуги. Например, вы можете нарисовать купол на вершине прямоугольника, используя Path и метод addArc.
В приведенном выше коде мы использовали Path для рисования прямоугольника и контура. Если вы хотите добавить купол на вершину прямоугольника, вы можете использовать метод addArc, чтобы создать дугу, соединяющую две верхние точки прямоугольника. Например:
struct ContentView: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 300, y: 20))
path.addLine(to: CGPoint(x: 300, y: 200))
path.addLine(to: CGPoint(x: 20, y: 200))
path.addArc(center: CGPoint(x: 160, y: 200), radius: 140, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 180), clockwise: false)
path.closeSubpath()
}
.stroke(Color.green, lineWidth: 2)
}
}
В SwiftUI вы можете использовать метод addQuadCurve для рисования кривых, определяя якорные и контрольные точки. Якорные точки определяют начало и конец кривой, а контрольная точка определяет форму кривой. Например, вы можете нарисовать угол оттягивающийся не с середины линии, используя Path и метод addQuadCurve, определив якорные и контрольные точки.
Если вы хотите использовать метод addQuadCurve для рисования углов с точками контроля, вы можете определить якорные и контрольные точки, как показано ниже:
struct ContentView: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 300, y: 20))
path.addLine(to: CGPoint(x: 300, y: 200))
path.addLine(to: CGPoint(x: 20, y: 200))
path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 90))
path.addLine(to: CGPoint(x: 40, y: 60))
path.closeSubpath()
}
.stroke(Color.green, lineWidth: 2)
}
}
И наконец метод addCurve позволяет рисовать более сложные кривые, определяя несколько контрольных точек. В отличие от метода addQuadCurve, который использует одну контрольную точку, метод addCurve использует три или более контрольных точек для создания кривой.
Например, вы можете нарисовать волнообразную линию, используя Path и метод addCurve, определив несколько контрольных точек. В приведенном ниже коде мы использовали Path для рисования волнообразной линии, определив три контрольных точки:
struct ContentView: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 20, y: 200))
path.addCurve(to: CGPoint(x: 120, y: 100),
control1: CGPoint(x: 70, y: 250), control2: CGPoint(x: 100, y: 50))
path.addCurve(to: CGPoint(x: 220, y: 200),
control1: CGPoint(x: 140, y: 150), control2: CGPoint(x: 180, y: 250))
path.addCurve(to: CGPoint(x: 320, y: 100),
control1: CGPoint(x: 260, y: 150), control2: CGPoint(x: 290, y: 50))
}
.stroke(Color.green, lineWidth: 2)
}
}
Fill и Stroke
Что если вы хотите нарисовать границу фигуры и заполнить фигуру цветом одновременно? Модификаторы fill и stroke не могут быть использованы параллельно. Вы можете воспользоваться ZStack, чтобы добиться желаемого эффекта. Вот код:
struct ContentView: View {
var body: some View {
ZStack {
// Draw the filled shape
Path { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 200, y: 20))
path.addQuadCurve(to: CGPoint(x: 110, y: 100), control: CGPoint(x: 200, y: 90))
path.addLine(to: CGPoint(x: 20, y: 20))
}
.fill(Color.purple)
// Draw the border of the shape
Path { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 200, y: 20))
path.addQuadCurve(to: CGPoint(x: 110, y: 100), control: CGPoint(x: 200, y: 90))
path.addLine(to: CGPoint(x: 20, y: 20))
}
.stroke(Color.black, lineWidth: 4)
}
}
}
В SwiftUI вы можете использовать ZStack для наложения одного объекта Path на другой, чтобы рисовать границу фигуры и заполнять фигуру цветом одновременно. В приведенном выше коде мы создали два объекта Path с одинаковым путем и наложили один на другой с помощью ZStack. Нижний объект Path использует модификатор fill, чтобы заполнить прямоугольник с куполом фиолетовым цветом. Верхний объект Path использует модификатор stroke, чтобы нарисовать только границы черным цветом. Это простой пример использования ZStack для рисования границы фигуры и заполнения фигуры цветом одновременно в SwiftUI. Вы можете использовать Path для создания более сложных форм и использовать ZStack для наложения одного объекта Path на другой, чтобы рисовать границу фигуры и заполнять фигуру цветом одновременно.
Рисование дуг и диаграмм
SwiftUI предоставляет удобный API для разработчиков, чтобы рисовать дуги. Этот API очень полезен для создания различных фигур и объектов, включая круговые диаграммы. Чтобы нарисовать дугу, вы пишете код следующим образом:
struct ContentView: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 200, y: 200))
path.addArc(center: .init(x: 200, y: 200), radius: 150, startAngle: .degrees(0), endAngle: .degrees(90), clockwise: true)
}
.fill(.green)
}
}
В данном коде мы сначала перемещаемся в точку (200, 200) с помощью метода move(to:). Затем мы добавляем дугу с помощью метода addArc(center:radius:startAngle:endAngle:clockwise:). В качестве параметров мы передаем центр дуги, радиус, начальный и конечный углы, а также направление рисования дуги (по или против часовой стрелки). В данном случае мы рисуем дугу от 0 до 90 градусов по часовой стрелке и заполняем ее зеленым цветом с помощью модификатора .fill(.green).
С помощью функции addArc вы можете легко создать диаграмму кругового графика с разноцветными сегментами. Для этого достаточно наложить друг на друга разные сегменты круговой диаграммы с помощью ZStack. Каждый сегмент имеет разные значения для startAngle и endAngle, чтобы составить диаграмму. Вот пример:
struct ContentView: View {
var body: some View {
ZStack {
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187),
radius: 150, startAngle: .degrees(0),
endAngle: .degrees(190), clockwise: true)
}
.fill(.yellow)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187),
radius: 150, startAngle: .degrees(190),
endAngle: .degrees(110), clockwise: true)
}
.fill(.teal)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187),
radius: 150, startAngle: .degrees(110),
endAngle: .degrees(90), clockwise: true)
}
.fill(.blue)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187),
radius: 150, startAngle: .degrees(90),
endAngle: .degrees(360), clockwise: true)
}
.fill(.purple)
}
}
}
Этот код создает круговую диаграмму из четырех сегментов. Каждый сегмент задается с помощью функции `Path`, которая создает путь, состоящий из одной дуги. Затем к каждому пути применяется функция `fill`, которая закрашивает путь определенным цветом.
Центр круга задается с помощью `CGPoint(x: 187, y: 187)`, что соответствует центру экрана в этом случае. Каждый сегмент задается с помощью функции `addArc`, которая принимает параметры `center`, `radius`, `startAngle` и `endAngle`. Здесь радиус равен 187, а углы задаются в градусах.
Первый сегмент закрашивается желтым цветом и занимает 190 градусов от 0 до 190. Второй сегмент закрашивается голубым цветом и занимает 80 градусов от 190 до 110. Третий сегмент закрашивается синим цветом и занимает 20 градусов от 110 до 90. Четвертый сегмент закрашивается фиолетовым цветом и занимает 270 градусов от 90 до 360.
В итоге получается круговая диаграмма, в которой каждый сегмент занимает определенный процент от общего круга.
В некоторых случаях вам может потребоваться выделить определенный сегмент, отделив его от круговой диаграммы. Например, чтобы выделить сегмент фиолетового цвета, вы можете применить модификатор offset, чтобы сместить сегмент:
struct ContentView: View {
var body: some View {
ZStack {
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187),
radius: 150, startAngle: .degrees(0),
endAngle: .degrees(190), clockwise: true)
}
.fill(.yellow)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187),
radius: 150, startAngle: .degrees(190),
endAngle: .degrees(110), clockwise: true)
}
.fill(.teal)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187),
radius: 150, startAngle: .degrees(110),
endAngle: .degrees(90), clockwise: true)
}
.fill(.blue)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90
), endAngle: .degrees(360), clockwise: true)
}
.fill(.purple)
.offset(x: 20, y: 20)
}
}
}
В этом примере сегмент фиолетового цвета смещается на 20 пикселей по оси X и на 20 пикселей по оси Y, чтобы выделить его из круговой диаграммы. Этот эффект может быть полезен, когда вы хотите обратить внимание на определенный сегмент или предоставить дополнительную информацию о нем.
Опционально вы можете добавить границу, чтобы еще больше привлечь внимание людей. Если вы хотите добавить метку к выделенному сегменту, вы также можете наложить текстовое представление следующим образом:
struct ContentView: View {
var body: some View {
ZStack {
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187),
radius: 150, startAngle: .degrees(0),
endAngle: .degrees(190), clockwise: true)
}
.fill(.yellow)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187),
radius: 150, startAngle: .degrees(190),
endAngle: .degrees(110), clockwise: true)
}
.fill(.teal)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187),
radius: 150, startAngle: .degrees(110),
endAngle: .degrees(90), clockwise: true)
}
.fill(.blue)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90
), endAngle: .degrees(360), clockwise: true)
}
.fill(.purple)
.offset(x: 20, y: 20)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90
), endAngle: .degrees(360), clockwise: true)
path.closeSubpath()
}
.stroke(Color(red: 52/255, green: 52/255, blue: 122/255), lineWidth: 10) .offset(x: 20, y: 20)
.overlay(
Text("25%")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundColor(.white)
.offset(x: 80, y: -110)
)
}
}
}
Разбираемся с протоколом Shape
Прежде чем изучать протокол Shape, давайте начнем с простого упражнения. На основе того, что вы уже изучили, нарисуйте следующую фигуру с помощью Path.
Не смотрите на решение сразу. Попробуйте сделать это самостоятельно.
Хорошо, чтобы сделать эту фигуру, вы создаете путь с помощью addRect и addQuadCurve:
struct ContentView: View {
var body: some View {
Path() { path in
path.move(to: CGPoint(x: 100, y: 100))
path.addQuadCurve(to: CGPoint(x: 300, y: 100), control: CGPoint(x: 200, y: 80))
path.addRect(CGRect(x: 100, y: 100, width: 200, height: 40))
}
.fill(Color.green)
}
}
В этом коде создается путь, который начинается с точки (100, 100). Затем добавляется квадратичная кривая Безье, которая проходит через точку (200, 80) и заканчивается в точке (300, 100). Затем добавляется прямоугольник размером 200x40, расположенный внизу кривой. Затем путь заполняется зеленым цветом.
Этот код создает фигуру, показанную на рисунке. Вы можете экспериментировать с параметрами кривой и прямоугольника, чтобы изменить форму фигуры.
Давайте поговорим о протоколе Shape. Протокол очень прост и содержит только одно требование. Для его реализации необходимо имплементировать следующую функцию:
func path(in rect: CGRect) -> Path
Когда полезно реализовывать протокол Shape? Чтобы ответить на этот вопрос, предположим, что вы хотите создать кнопку с куполообразной формой, но с динамическим размером. Можно ли повторно использовать путь, который вы только что создали?
Посмотрите еще раз на код выше. Вы создали путь с абсолютными координатами и размером. Чтобы создать ту же форму, но с переменным размером, вы можете создать структуру, реализующую протокол Shape, и имплементировать функцию path(in:). Когда функция path(in:) будет вызвана фреймворком, вам будет предоставлен размер прямоугольника. Затем вы можете нарисовать путь внутри этого прямоугольника.
Создадим такую структуру:
struct Dome: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: rect.size.width, y: 0), control: CGPoint( x: rect.size.width/2, y: -(rect.size.width * 0.1)))
path.addRect(CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height))
return path }
}
И добавим кнопку с такой формой:
struct ContentView: View {
var body: some View {
Button {
} label: {
Text("Dome Button")
.font(.system(.title, design: .rounded))
.bold()
.foregroundColor(.white)
.frame(width: 250, height: 50)
.background(Dome().fill(Color.red))
}
}
}
Мы применяем форму купола в качестве фона кнопки. Его ширина и высота основаны на указанном размере frame.
Используем встроенные Shapes (формы)
Мы создали настраиваемую форму с помощью протокола Shape, но на самом деле SwiftUI поставляется с несколькими встроенными формами, включая Circle, Rectangle, RoundedRectangle, Ellipse и т.д. Если вам не требуется ничего сложного, эти формы достаточно хороши для создания обычных объектов.
Давайте создадим такую импровизированную кнопку стоп
struct ContentView: View {
var body: some View {
Circle()
.foregroundColor(.green)
.frame(width: 200, height: 200)
.overlay(
RoundedRectangle(cornerRadius: 5)
.frame(width: 80, height: 80)
.foregroundColor(.white)
)
}
}
Как вы видите нам было достаточно просто достать RoundedRectangle который был любезно предоставлен нам в фреймворке SUI
Создаем индикатор прогресса используя Shapes
При помощи комбинирования и сочетания встроенных форм, вы можете создавать разнообразные типы векторных элементов управления пользовательским интерфейсом для ваших приложений. Рассмотрим еще один пример. На следующем скриншоте демонстрируется способ построения индикатора прогресса с использованием круга.
Данный индикатор прогресса представляет собой комбинацию двух кругов. В основании расположен серый контур круга. Над ним находится открытый контур круга, символизирующий прогресс выполнения. В вашем проекте необходимо записать код в ContentView следующим образом:
struct ContentView: View {
private var purpleGradient = LinearGradient(
gradient: Gradient(
colors: [ Color (red: 207/255, green: 150/255, blue: 207/255),
Color(red: 107/255, green: 116/255,
blue: 179/255) ]),
startPoint: .trailing, endPoint: .leading)
var body: some View {
ZStack {
Circle()
.stroke(Color(.systemGray6), lineWidth: 20)
.frame(width: 300, height: 300)
} }
}
Для отображения контура серого круга применяется модификатор stroke. В зависимости от ваших предпочтений, вы можете корректировать значение параметра lineWidth, чтобы изменить толщину линий.
Свойство purpleGradient задает фиолетовый градиент, который будет использован при рисовании открытого круга.
А теперь давайте добавим код который добавит круг который будет показывать заполнение:
struct ContentView: View {
private var purpleGradient = LinearGradient(
gradient: Gradient(
colors: [ Color (red: 207/255, green: 150/255, blue: 207/255),
Color(red: 107/255, green: 116/255,
blue: 179/255) ]),
startPoint: .trailing, endPoint: .leading)
var body: some View {
ZStack {
Circle()
.stroke(Color(.systemGray6), lineWidth: 20)
.frame(width: 300, height: 300)
Circle()
.trim(from: 0, to: 0.91)
.stroke(purpleGradient, lineWidth: 20)
.frame(width: 300, height: 300)
.overlay {
VStack {
Text("91%")
.font(.system(size: 80, weight: .bold, design: .rounded))
.foregroundColor(.gray)
Text("Complete")
.font(.system(.body, design: .rounded))
.bold()
.foregroundColor(.gray)
}
}
}
}
}
Для создания открытого круга необходимо добавить модификатор trim, указав значения от и до, чтобы определить, какой сегмент круга должен быть отображен. В данном случае мы хотим продемонстрировать прогресс в 91%. Для этого значение from устанавливается равным 0, а значение to - 0,91.
Для отображения процента выполнения мы расположили текстовое представление по центру круга.
Рисование диаграммы в виде пончика
Последним примером, который я хотел бы вам показать, является диаграмма типа "кольцо" (donut chart). Если вы уже поняли, как работает модификатор trim, вы, вероятно, уже знаете, как мы собираемся реализовать диаграмму типа "кольцо". Изменяя значения модификатора trim, мы можем разделить круг на несколько сегментов. Эту технику мы используем для создания диаграммы типа "кольцо", и вот ее код:
struct ContentView: View {
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.2)
.stroke(Color(.systemBlue), lineWidth: 80)
Circle()
.trim(from: 0.2, to: 0.43)
.stroke(Color(.systemTeal), lineWidth: 80)
Circle()
.trim(from: 0.43, to: 0.70)
.stroke(Color(.systemPurple), lineWidth: 80)
Circle()
.trim(from: 0.70, to: 1)
.stroke(Color(.systemGreen), lineWidth: 90)
.overlay(
Text("30%")
.font(.system(.title, design: .rounded))
.bold()
.foregroundColor(.white)
.offset(x: 80, y: -100)
)
}
.frame(width: 250, height: 250)
}
}
Этот код создает диаграмму типа "кольцо" (donut chart) с четырьмя сегментами разных цветов. Каждый сегмент представляет собой круг, к которому применен модификатор trim для разделения его на определенный сегмент.
После этого каждый сегмент закрашивается определенным цветом с помощью модификатора stroke. Толщина линии каждого сегмента задается с помощью параметра lineWidth.
В конце кода располагается текстовое представление, которое отображается поверх последнего сегмента и показывает процентное соотношение. Текст смещается с помощью модификатора offset.
В итоге, этот код создает диаграмму типа "кольцо" с четырьмя сегментами, разделенными с помощью модификатора trim, и с текстовым представлением, отображающим процентное соотношение.
Итоги
Мы посмотрели на то как можно использовать Path и Shape для создания различных собственных форм или как взять те которые выдает нам Apple в SUI, я думаю самые внимательные из вас - те кто следят за тем как развивается этот фреймворк знают что есть такой модуль Charts c помощью которого можно рисовать различные графики и это удобнее чем использовать Path и Shape, например так:
import SwiftUI
import Charts
struct DataPoint: Identifiable {
let id: String
let value: Double
}
let data: [DataPoint] = [
DataPoint(id: "January", value: 23.0),
DataPoint(id: "February", value: 45.0),
DataPoint(id: "March", value: 32.0),
DataPoint(id: "April", value: 56.0),
DataPoint(id: "May", value: 28.0),
DataPoint(id: "June", value: 70.0)
]
struct ContentView: View {
var body: some View {
Chart(data) { dataPoint in
BarMark(
x: .value("Month", dataPoint.id),
y: .value("Value", dataPoint.value)
)
}
}
}
Однако, надо понимать - в этой части мы занимались не графиками, а изучали, то как вообще можно рисовать различные фигуры и надеюсь что вы научились этому прочитав эту статью!
Как и прежде подписывайтесь на мой телеграм канал - https://t.me/swiftexplorer
В ближайшее время выйдут следующие части уроков по SwiftUI.
Спасибо за прочтение!