Skip to content

Instantly share code, notes, and snippets.

@mortenjust
Created January 20, 2023 14:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mortenjust/cd662419d9ff5f5eea688930869c73a6 to your computer and use it in GitHub Desktop.
Save mortenjust/cd662419d9ff5f5eea688930869c73a6 to your computer and use it in GitHub Desktop.
import SwiftUI
// MARK: Main
struct TwitterBluePlus: View {
@State var topMenuItems : [MenuItem] = [MenuItem(title: "For you", icon: nil), MenuItem(title: "Following", icon: nil)]
@State var selectedIdx = 0
var selected : MenuItem { topMenuItems[selectedIdx] }
let twitterColor = Color(hue:0.585, saturation:0.502, brightness:0.165)
var body: some View {
ZStack {
VStack(spacing:0) {
// Time bar
CropImage("Following", fRect: CGRect(x: 0, y: 0.0, width: 1, height: 0.06))
// Profile photo
CropImage("For you", fRect: CGRect(x: 0, y: 0.06, width: 1, height: 0.060))
// Menu
TopMenu(items: $topMenuItems)
// Tweets
CropImage(selected.title, fRect: CGRect(x: 0, y: 0.153, width: 1, height: 0.8))
.transition(.move(edge: .leading))
.animation(.default, value: selected)
Spacer()
}
.frame(width: 500, height: 1000)
.background(twitterColor)
}.padding(50)
}
}
// MARK: Top menu item
struct TopMenuItemView : View {
let item: MenuItem
let isEditing : Bool
@Binding var items : [MenuItem]
@State var isDown = false
func remove(_ item : MenuItem) {
withAnimation {
items.removeAll { $0.title == item.title }
}
}
var body: some View {
VStack {
HStack {
if isEditing && items.count > 1{
Button {
remove(item)
} label: {
Image(systemName: "x.circle.fill")
.offset(y: -5)
}
.contentShape(Rectangle())
.buttonStyle(.plain)
.transition(.scale
.combined(with: .opacity)
)
}
Text(item.title)
.font(.title.bold())
.fixedSize(horizontal: true, vertical: true)
.opacity(isDown ? 0.8 : 1)
}
ZStack {
if item == items.first { // should be Selected
Capsule()
.fill(Color(hue:0.566, saturation:0.889, brightness:0.936))
}
}
.frame(height: 4)
}.onHover { h in
isDown = h
}
}
}
// MARK: Top menu
struct TopMenu : View {
@State var isEditing = false
@Binding var items : [MenuItem]
@State var draggingItem : MenuItem?
@State var dragItemLocation : CGPoint?
@State var hasChangedLocation = false
var body: some View {
VStack(spacing:0) {
HStack {
Group {
HStack(spacing: 17) {
ForEach(items) { item in
TopMenuItemView(item: item, isEditing: isEditing, items: $items)
.frame(maxWidth: 100)
.transition(.dropToFloor)
.wiggle(isEditing)
.padding(.leading, 17)
.onDrag {
return .init(object: NSString(string: item.id))
} preview: {
Rectangle().frame(width: .zero)
}
.onDrop(of: [.utf8PlainText], delegate: MenuDropDelegate(item: item, current: $draggingItem, items: $items, hasChangedLocation: $hasChangedLocation, itemLocation: $dragItemLocation))
}
Spacer()
}
.animation(.default, value: items)
.contentShape(Rectangle())
.simultaneousGesture(
LongPressGesture()
.onEnded({ ended in
withAnimation(.interactiveSpring()) {
isEditing = true
}
})
.onChanged({ changed in
print("longpressiez", changed)
})
)
.frame(maxWidth: .infinity)
.clipped()
}
if isEditing {
Button {
withAnimation(.default) {
isEditing = false
}
} label: {
Image(systemName: "checkmark")
.font(.title.bold())
}.buttonStyle(.plain)
.padding()
}
}
// Hairline
Rectangle().fill(.white.opacity(0.2)).frame(height: 0.5)
}
}
}
// MARK: Preview
struct TwitterBluePlus_Previews: PreviewProvider {
static var previews: some View {
TwitterBluePlus()
}
}
// MARK: Model
struct MenuItem : Identifiable, Equatable {
var id = UUID().uuidString
let title : String
let icon : Image?
}
// MARK: View Modifier
extension View {
func wiggle(_ active : Bool) -> some View {
self.modifier(WiggleMode(active: active))
}
}
struct WiggleMode : ViewModifier {
let active : Bool
@State var go = false
let rota = 2.0
func body(content: Content) -> some View {
let a = Animation.linear(duration: 0.15)
content
.rotationEffect(.degrees(active ? (go ? rota : -rota) : 0))
.onChange(of: active) { newValue in
withAnimation(active
? a
: a.repeatForever(autoreverses: true).delay(0.5 * Double.random(in: 0...1))) {
print("turning on:", newValue)
go = newValue
}
}
}
}
struct MenuDropDelegate : DropDelegate {
let item : MenuItem
@Binding var current: MenuItem?
@Binding var items: [MenuItem]
@Binding var hasChangedLocation : Bool
@Binding var itemLocation : CGPoint?
func dropEntered(info: DropInfo) {
if current == nil {
current = item
itemLocation = info.location
}
guard item != current, let current, let from = items.firstIndex(of: current), let toIndex = items.firstIndex(of: item) else { return }
hasChangedLocation = true
itemLocation = info.location
if items[toIndex] != current {
items.move(fromOffsets: IndexSet(integer: from), toOffset: toIndex > from ? toIndex + 1 : toIndex)
}
}
func dropExited(info: DropInfo) {
itemLocation = nil
}
func dropUpdated(info: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
hasChangedLocation = false
itemLocation = nil
current = nil
return true
}
}
// MARK: Crop image
struct CropImage : View {
let image : CGImage
init(_ name : String, fRect rect: CGRect) {
let image = NSImage(named: name)!
let cg = image.cgImage(forProposedRect: nil, context: nil, hints: nil)!
let w = CGFloat(cg.width)
let h = CGFloat(cg.height)
let origin = CGPoint(x: rect.origin.x * w, y: rect.origin.y * h)
let size = CGSize(width: rect.width * w, height: rect.height * h)
let cropped = cg.cropping(to: CGRect(origin: origin, size: size))!
self.image = cropped
}
var body: some View {
Image(image, scale: 1, label: Text("Image"))
.resizable()
.aspectRatio(contentMode: .fit)
}
}
struct DropModifier : ViewModifier {
let i : Double
@State var blur : CGFloat = 0
func body(content: Content) -> some View {
content
.opacity(1 - i)
.offset(y: i == 1 ? 100 : 0)
}
}
extension AnyTransition {
static var dropToFloor: AnyTransition {
.modifier(active: DropModifier(i:1),
identity: DropModifier(i:0))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment