Skip to content

Instantly share code, notes, and snippets.

@rusik
Last active October 22, 2022 17:39
Show Gist options
  • Save rusik/a7b18c9d3741e4851e2150137fa4dafd to your computer and use it in GitHub Desktop.
Save rusik/a7b18c9d3741e4851e2150137fa4dafd to your computer and use it in GitHub Desktop.
Implementation of half-pizza UI from Dodo Pizza on SwiftUI. Original UIKit implementation is here → https://habr.com/ru/company/dododev/blog/452876/
import SwiftUI
import Foundation
struct ContentView: View {
var body: some View {
HStack(spacing: 4) {
PizzaView(color: .blue, orientation: .left)
PizzaView(color: .red, orientation: .right)
}
}
}
struct PizzaView: View {
var color: Color = .gray
var models: [Int] = [0, 1, 2, 3, 4, 5]
var orientation: HalfCircle.Orientation = .right
private let widht: CGFloat = 300
@State private var idx: Int = 0
@State private var positions: [Int: CGFloat] = [:]
var body: some View {
PagerView(pageCount: models.count, currentIndex: $idx) {
ForEach(models, id: \.self) { idx in
let halfWidth = widht / 2
let percent = min(1, max(0, (halfWidth - abs(positions[idx, default: 0] - halfWidth)) / halfWidth))
let scale = 0.5 + percent * 0.5
HalfCircle(orientation: orientation)
.frame(width: widht, height: widht)
.foregroundColor(color)
.rotationEffect(.degrees(270))
.background(
GeometryReader { proxy in
Color.clear.preference(
key: PositionPreferenceKey.self,
value: [idx: proxy.frame(in: .named("view")).midY]
)
}
)
.scaleEffect(scale, anchor: orientation == .right ? .bottom : .top)
.opacity(scale)
}
}
.onPreferenceChange(PositionPreferenceKey.self) {
self.positions = $0
}
.frame(width: widht, height: widht)
.rotationEffect(.degrees(90))
.coordinateSpace(name: "view")
}
}
struct HalfCircle: Shape {
let orientation: Orientation
enum Orientation {
case left, right
}
func path(in rect: CGRect) -> Path {
var path = Path()
let radius = min(rect.width, rect.height) / 2
let startPoint = CGPoint(
x: orientation == .right ? rect.minX : rect.maxX,
y: rect.minY
)
let center = CGPoint(
x: orientation == .right ? rect.minX : rect.maxX,
y: rect.midY
)
path.move(to: startPoint)
path.addArc(
center: center,
radius: radius,
startAngle: .degrees(90),
endAngle: .degrees(270),
clockwise: orientation == .right,
transform: .identity
)
return path
}
}
struct PagerView<Content: View>: View {
let pageCount: Int
@State var ignore: Bool = false
@Binding var currentIndex: Int {
didSet {
if (!ignore) {
currentFloatIndex = CGFloat(currentIndex)
}
}
}
@State var currentFloatIndex: CGFloat = 0 {
didSet {
ignore = true
currentIndex = min(max(Int(currentFloatIndex.rounded()), 0), self.pageCount - 1)
ignore = false
}
}
let content: Content
@GestureState private var offsetX: CGFloat = 0
init(pageCount: Int, currentIndex: Binding<Int>, @ViewBuilder content: () -> Content) {
self.pageCount = pageCount
self._currentIndex = currentIndex
self.content = content()
}
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
self.content.frame(width: geometry.size.width)
}
.frame(width: geometry.size.width, alignment: .leading)
.offset(x: -CGFloat(self.currentFloatIndex) * geometry.size.width)
.offset(x: self.offsetX)
.animation(.linear, value:offsetX)
.highPriorityGesture(
DragGesture().updating(self.$offsetX) { value, state, _ in
state = value.translation.width
}
.onEnded({ (value) in
let offset = value.translation.width / geometry.size.width
let offsetPredicted = value.predictedEndTranslation.width / geometry.size.width
let newIndex = CGFloat(self.currentFloatIndex) - offset
self.currentFloatIndex = newIndex
withAnimation(.easeOut) {
if(offsetPredicted < -0.5 && offset > -0.5) {
self.currentFloatIndex = CGFloat(min(max(Int(newIndex.rounded() + 1), 0), self.pageCount - 1))
} else if (offsetPredicted > 0.5 && offset < 0.5) {
self.currentFloatIndex = CGFloat(min(max(Int(newIndex.rounded() - 1), 0), self.pageCount - 1))
} else {
self.currentFloatIndex = CGFloat(min(max(Int(newIndex.rounded()), 0), self.pageCount - 1))
}
}
})
)
}
.onChange(of: currentIndex, perform: { value in
withAnimation(.easeOut) {
currentFloatIndex = CGFloat(value)
}
})
}
}
private struct PositionPreferenceKey: PreferenceKey {
static var defaultValue: [Int: CGFloat] = [:]
static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) {
for (key, val) in nextValue() {
value[key] = val
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment