Нарисуйте прямую линию между двумя кругами в SwiftUI

Я пробовал разные способы рисования линии между двумя кругами (первым и вторым) в SwiftUI.

Мой взгляд — это, по сути, сетка из 50 кругов в сетке 10 x 5.

Я нашел несколько других тем об использовании ключей предпочтений и еще одного предложенного GeometryReader, но применение его к моему представлению, похоже, не помогает. Я решил реализовать собственный способ поиска координат CGPoint для каждого круга в моем представлении, и он работает, но не так, как я хочу.

Мое намерение состоит в том, чтобы найти CGPoint центра каждого круга в сетке, чтобы я мог равномерно рисовать линии к каждому центру круга, однако я просто не понимаю правильных вычислений, может ли кто-нибудь помочь мне указать, что здесь не так?

Это то, что у меня есть на данный момент, и calculateCirclePosition() так я вычисляю каждую координату.

Это мой код просмотра ниже для справки ниже:

import SwiftUI

struct CircleGridView: View {
    let rows = 10
    let columns = 5
    @State private var coordinates: [Int :CGPoint] = [:]
    
    var body: some View {        
        ZStack {    
            LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columns)) {
                ForEach(0..<rows * columns, id: \.self) { index in                    
                    Circle()
                        .foregroundColor(.blue)
                        .frame(width: 50, height: 50)
                        .onAppear {
                            DispatchQueue.main.async {
                                let circlePosition = calculateCirclePosition(for: index)
                                coordinates[index] = circlePosition
                                print("Index \(index): \(circlePosition)")
                            }
                        }
                }
            }
            .padding(10)
            if let startPoint = coordinates[0], let endPoint = coordinates[1] {
                Path { path in 
                path.move(to: startPoint)
                path.addLine(to:  endPoint)
            }.stroke(Color.red, lineWidth: 2)
        }
        }
    }
        
        func calculateCirclePosition(for index: Int) -> CGPoint {
            let row = index / columns
            let column = index % columns
            let circleSize: CGFloat = 50
            let circleSpacing: CGFloat = 10 
            let yOffset: CGFloat = 85
            let xOffset: CGFloat = 15
            
            let x = CGFloat(column) * (circleSize + circleSpacing) + circleSize / 2 + circleSpacing + xOffset
            let y = CGFloat(row) * (circleSize + circleSpacing) + circleSize / 2 + circleSpacing + yOffset
            
            return CGPoint(x: x, y: y)
        }
}

Гораздо проще это сделать, нарисовав Canvas. Есть ли проблемы с использованием Canvas для рисования кругов и линий?

Sweeper 10.05.2024 09:09
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
145
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вы можете использовать GeometryReader для чтения рамок кругов в координатном пространстве ZStack (которое является координатным пространством, в котором нарисован Path).

Вместо использования onAppear для обновления словаря для отслеживания центров кругов поместите словарь в PreferenceKey:

struct CirclePositionsKey: PreferenceKey {
    static var defaultValue: [Int: CGPoint] { [:] }
    
    static func reduce(value: inout [Int : CGPoint], nextValue: () -> [Int : CGPoint]) {
        value.merge(nextValue(), uniquingKeysWith: { $1 })
    }
}

Предпочтением каждого круга будет одна пара ключ-значение. Реализация reduce объединяет словари предпочтений всех кругов вместе, так что мы получаем весь словарь в overlayPreferenceValue, где мы можем нарисовать путь в виде наложения.

В представлении вы можете сделать:

let rows = 10
let columns = 5

var body: some View {
    LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columns)) {
        ForEach(0..<rows * columns, id: \.self) { index in
            GeometryReader { geo in
                let frame = geo.frame(in: .named("DrawingSpace"))
                // read the circle centre here
                let center = CGPoint(x: frame.midX, y: frame.midY)
                Circle()
                    .foregroundColor(.blue)
                    .preference(key: CirclePositionsKey.self, value: [
                        index: center
                    ])
            }
            .frame(width: 50, height: 50)
        }
    }
    .padding(10)
    .overlayPreferenceValue(CirclePositionsKey.self) { value in
        // draw the path as an overlay, based on the preference value
        if let p1 = value[0], let p2 = value[1] {
            Path { path in
                path.move(to: p1)
                path.addLine(to: p2)
            }.stroke(Color.red, lineWidth: 2)
        }
    }
    .coordinateSpace(.named("DrawingSpace"))
}

Тем не менее, вместо этого я бы использовал Canvas, чтобы рисовать круги и линии. Canvas дает вам гораздо больше контроля над тем, где именно рисовать.

Например:

let rows = 10
let columns = 5

var body: some View {
    Canvas { gc, size in
        let radius: CGFloat = 25
        let spacing: CGFloat = 10
        
        // calculate size of the things to draw...
        let totalWidth = CGFloat(columns) * (radius * 2 + spacing) - spacing
        let totalHeight = CGFloat(rows) * (radius * 2 + spacing) - spacing
        // so that we can calculate some offsets in order to center-align the whole drawing
        let xOffset = (size.width - totalWidth) / 2
        let yOffset = (size.height - totalHeight) / 2
        gc.translateBy(x: xOffset, y: yOffset)
        
        for i in 0..<rows {
            for j in 0..<columns {
                let frame = CGRect(
                    x: CGFloat(j) * (radius * 2 + spacing),
                    y: CGFloat(i) * (radius * 2 + spacing),
                    width: radius * 2,
                    height: radius * 2
                )
                gc.fill(Path(ellipseIn: frame), with: .color(.blue))
            }
        }
        
        func centerOfCircle(atColumn col: Int, row: Int) -> CGPoint {
            let x = CGFloat(col) * (radius * 2 + spacing) + radius
            let y = CGFloat(row) * (radius * 2 + spacing) + radius
            return CGPoint(x: x, y: y)
        }
        
        let line = Path {
            $0.move(to: centerOfCircle(atColumn: 0, row: 0))
            $0.addLine(to: centerOfCircle(atColumn: 1, row: 0))
        }
        
        gc.stroke(line, with: .color(.red), lineWidth: 2)
    }
}

Canvas выглядит как отличный вариант для изучения. Я проверю это. Спасибо за ваши решения по обоим предложениям, они выглядят хорошо и работают для меня. Я думаю, что буду больше изучать пользователя Canvas.

Jeevan Daniel Mahtani 10.05.2024 11:06

Как прокомментировал Sweeper, более простой способ нарисовать сетку — использовать Canvas.

Однако, если вы действительно хотите использовать LazyVGrid, то линии можно просто нарисовать, наложив их на круги. Таким образом, позиционирование происходит автоматически и вычисления полностью исключаются.

  • Знак GeometryReader можно использовать, чтобы найти размер каждой ячейки сетки.
  • Смещение можно использовать для перекрытия с предыдущим кругом в строке или столбце.
  • Необходимо внести корректировку в шаг сетки. В качестве альтернативы вы можете установить интервал равным 0 и вместо этого использовать отступы вокруг кругов.
struct CircleGridView: View {
    let rows = 10
    let columns = 5
    let spacing: CGFloat = 10

    var body: some View {
        ZStack {
            LazyVGrid(
                columns: Array(
                    repeating: GridItem(.flexible(), spacing: spacing),
                    count: columns
                ),
                spacing: spacing
            ) {
                ForEach(0..<rows * columns, id: \.self) { index in
                    Circle()
                        .foregroundColor(.blue)
                        .frame(width: 50, height: 50)
                        .frame(maxWidth: .infinity)
                        .overlay {
                            GeometryReader { proxy in
                                ZStack {
                                    if (index / columns) > 0 {
                                        Color.orange
                                            .frame(width: 2)
                                            .offset(y: -(proxy.size.height + spacing) / 2)
                                    }
                                    if !index.isMultiple(of: columns) {
                                        Color.red
                                            .frame(height: 2)
                                            .offset(x: -(proxy.size.width + spacing) / 2)
                                    }
                                }
                                .frame(maxWidth: .infinity, maxHeight: .infinity)
                            }
                        }
                }
            }
            .padding(10)
        }
    }
}

Screenshot

Спасибо за ваше решение. И да, Canvas выглядит как отличный вариант для изучения, а не для выполнения подобных вычислений.

Jeevan Daniel Mahtani 10.05.2024 11:08

Другие вопросы по теме