Рисуем шарик

Создаем новый файл для нашего шарика. Для этого я «правой кнопкой мышки» вызвал меню и выбрал New File… либо можно было бы нажать Command N. Новый файл разместится в списке под тем файлом, который я выбрал

Из шаблонов выбираем SwiftUI View

И даем название BallView

Новый файл разместился под ContentView.swift

строку Text меняем на Circle

struct BallView: View {
  var body: some View {
    Circle()
  }
}

Объемный шарик рассчитываю получить за счет градиента, для этого начинаю писать строчку fill(radial… по мере написания Xcode будет давать мне подсказки, дал подсказку RadialGradient, я согласился нажав Enter, затем открыл скобку, мне опять дали подсказку параметров

получил модификатор, в котором мне осталось только добавить значения параметров

Вот такие значения подставил

    Circle()
      .fill(RadialGradient(
        gradient: Gradient(colors: [
          Color.white,
          Color.gray
        ]),
        center: .init(x: 0.65, y: 0.35),
        startRadius: 0.09 * 414,
        endRadius: 0.5 * 414))

затем применил colorMultiply и получил шарик указанного цвета

    .colorMultiply(.green)

Для игры мне понадобится 7 цветов, их я собираюсь создать в Assets.xcassets и использовать по имени. Правой кнопкой вызываю меню и выбираю New Color Set

Даю название и выбираю цвет, можно из готовых палитр, а можно вставлять значения в шестнадцатиричном формате

Подобрать цвета можно здесь: https://colorhunt.co

Вот мои цвета:

FF40FF
008F51
0432FF
FF9200
F7F025
F02228
0096FF

Скачать цвета CirleColor.zip

Чтоб не ошибиться с названием цвета, их можно так же выбирать из библиотеки

Добавил переменную color, которая принимает произвольное целое число от 1 до 7, по нему будем вызывать цвета. Переменная width — ширина ячейки, которую будет занимать шарик, а scale будет изменять размер шарика

struct BallView: View {
  var color = Int.random(in: 1...7)
  var width: CGFloat = 414
  var scale: CGFloat = 0.7
  var body: some View {
    Circle()
      .fill(RadialGradient(
        gradient: Gradient(colors: [
          Color("CirleLightColor"),
          Color("CirleDarkColor")
        ]),
        center: .init(x: 0.65, y: 0.35),
        startRadius: 0.09 * width,
        endRadius: 0.5 * width))
      .colorMultiply(Color("CirleColor\(color)"))
      .frame(width: width, height: width)
      .scaleEffect(scale)
  }
}

А можем ли мы перетягивать шарик по экрану? Да! Для этого нам понадобится

@State var ballState = CGSize.zero

и несколько модификаторов:

      .offset(ballState)
      .gesture(
        DragGesture()
          .onChanged { value in
            self.ballState = value.translation
        }
      )

А можем ли мы заставить шарик прыгать, к примеру когда мы на него нажимаем? Да! Для этого нам понадобится

@State var selected = false

и несколько строчек кода

      .offset(y: selected ? -width / 8 : 0)
      .animation(selected
        ? Animation
          .default
          .repeatForever(autoreverses: true)
        : .default
      )
      .onTapGesture {
        self.selected.toggle()
      }

Но этот код будет работать, если устройство будет работать в одном положении, если при анимации совершить поворот, то ничего хорошего у нас не получится:) Прийдется использовать собственный эффект:

struct JumpEffect: GeometryEffect {
  var y: CGFloat = 0
  var animatableData: CGFloat {
    get { y }
    set { y = newValue }
  }
  func effectValue(size: CGSize) -> ProjectionTransform {
    ProjectionTransform(CGAffineTransform(translationX: 0, y: y))
  }
}
struct BallView_Previews: PreviewProvider {
  static var previews: some View {
    BallView()
  }
}

Тогда наш код будет выглядеть так:

      .modifier(JumpEffect(y: selected ? -width / 8 : 0))
      .onTapGesture {
        withAnimation(self.selected ? .default : Animation.easeInOut.repeatForever()) {
          self.selected.toggle()
        }
      }

Добавим еще пару переменных, которые будут отвечать за позицию шарика на игровом поле (т.е. в какой строке и в каком столбце находится шарик)

  var x: CGFloat = 0.0
  var y: CGFloat = 0.0
      .offset(x: x * width, y: y * width)
struct BallView: View {
  @State var selected = false
  @State var ballState = CGSize.zero
  var color = Int.random(in: 1...7)
  var width: CGFloat = 414
  var x: CGFloat = 0.0
  var y: CGFloat = 0.0
  var scale: CGFloat = 0.7
  var body: some View {
    Circle()
      .fill(RadialGradient(
        gradient: Gradient(colors: [
          Color("CirleLightColor"),
          Color("CirleDarkColor")
        ]),
        center: .init(x: 0.65, y: 0.35),
        startRadius: 0.09 * width,
        endRadius: 0.5 * width))
      .colorMultiply(Color("CirleColor\(color)"))
      .frame(width: width, height: width)
      .scaleEffect(scale)
      .offset(x: x * width, y: y * width)
      .offset(ballState)
      .modifier(JumpEffect(y: selected ? -width / 8 : 0))
      .onTapGesture {
        withAnimation(self.selected ? .default : Animation.easeInOut.repeatForever()) {
          self.selected.toggle()
        }
      }
      .gesture(
        DragGesture()
          .onChanged { value in
            self.ballState = value.translation
        }
      )
  }
}

Скачать iLines02.zip

      BallView(width: 414/9)