Add a native iOS client for relay-based file sharing
The SwiftUI app bundles the Go swarm engine through gomobile, so an iPhone is a real peer using the existing PAKE handshake, chunk verification, and relay fallback. XcodeGen builds the project and devicectl installs it on the paired phone.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env swift
|
||||
// Draws the 1024x1024 Share app icon. Run from apps/ios:
|
||||
// swift Branding/make-icon.swift Share/Assets.xcassets/AppIcon.appiconset/AppIcon.png
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import ImageIO
|
||||
|
||||
let size = 1024
|
||||
let output = CommandLine.arguments.count > 1
|
||||
? CommandLine.arguments[1]
|
||||
: "Share/Assets.xcassets/AppIcon.appiconset/AppIcon.png"
|
||||
|
||||
func color(_ hex: UInt32, _ alpha: CGFloat = 1) -> NSColor {
|
||||
NSColor(
|
||||
calibratedRed: CGFloat((hex >> 16) & 0xFF) / 255,
|
||||
green: CGFloat((hex >> 8) & 0xFF) / 255,
|
||||
blue: CGFloat(hex & 0xFF) / 255,
|
||||
alpha: alpha
|
||||
)
|
||||
}
|
||||
|
||||
guard let rep = NSBitmapImageRep(
|
||||
bitmapDataPlanes: nil,
|
||||
pixelsWide: size,
|
||||
pixelsHigh: size,
|
||||
bitsPerSample: 8,
|
||||
samplesPerPixel: 4,
|
||||
hasAlpha: true,
|
||||
isPlanar: false,
|
||||
colorSpaceName: .deviceRGB,
|
||||
bytesPerRow: 0,
|
||||
bitsPerPixel: 0
|
||||
) else {
|
||||
fatalError("Could not create bitmap")
|
||||
}
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
|
||||
NSGraphicsContext.current?.imageInterpolation = .high
|
||||
NSGraphicsContext.current?.shouldAntialias = true
|
||||
|
||||
let canvas = NSRect(x: 0, y: 0, width: size, height: size)
|
||||
let background = NSGradient(colors: [color(0x07111F), color(0x0B3B36)])!
|
||||
background.draw(in: canvas, angle: -55)
|
||||
|
||||
// Soft teal glow in the upper-left and blue glow in the lower-right.
|
||||
let tealGlow = NSGradient(colors: [color(0x5EEAD4, 0.35), color(0x5EEAD4, 0)])!
|
||||
tealGlow.draw(
|
||||
in: NSBezierPath(ovalIn: NSRect(x: -260, y: 430, width: 1020, height: 1020)),
|
||||
relativeCenterPosition: NSPoint(x: 0, y: 0)
|
||||
)
|
||||
let blueGlow = NSGradient(colors: [color(0x60A5FA, 0.28), color(0x60A5FA, 0)])!
|
||||
blueGlow.draw(
|
||||
in: NSBezierPath(ovalIn: NSRect(x: 300, y: -300, width: 980, height: 980)),
|
||||
relativeCenterPosition: NSPoint(x: 0, y: 0)
|
||||
)
|
||||
|
||||
// A barely-there inset panel gives the flat icon some depth.
|
||||
let panel = NSBezierPath(
|
||||
roundedRect: NSRect(x: 116, y: 116, width: 792, height: 792),
|
||||
xRadius: 180,
|
||||
yRadius: 180
|
||||
)
|
||||
color(0xFFFFFF, 0.035).setFill()
|
||||
panel.fill()
|
||||
color(0xFFFFFF, 0.07).setStroke()
|
||||
panel.lineWidth = 2
|
||||
panel.stroke()
|
||||
|
||||
func strokeArrow(shaftFrom: NSPoint, shaftTo: NSPoint,
|
||||
headLeft: NSPoint, headTip: NSPoint, headRight: NSPoint,
|
||||
tint: NSColor) {
|
||||
let shadow = NSShadow()
|
||||
shadow.shadowColor = color(0x000000, 0.3)
|
||||
shadow.shadowBlurRadius = 34
|
||||
shadow.shadowOffset = NSSize(width: 0, height: -16)
|
||||
shadow.set()
|
||||
|
||||
tint.setStroke()
|
||||
let shaft = NSBezierPath()
|
||||
shaft.move(to: shaftFrom)
|
||||
shaft.line(to: shaftTo)
|
||||
shaft.lineWidth = 74
|
||||
shaft.lineCapStyle = .round
|
||||
shaft.stroke()
|
||||
|
||||
let head = NSBezierPath()
|
||||
head.move(to: headLeft)
|
||||
head.line(to: headTip)
|
||||
head.line(to: headRight)
|
||||
head.lineWidth = 78
|
||||
head.lineCapStyle = .round
|
||||
head.lineJoinStyle = .round
|
||||
head.stroke()
|
||||
}
|
||||
|
||||
// Up arrow on the left.
|
||||
strokeArrow(
|
||||
shaftFrom: NSPoint(x: 350, y: 288),
|
||||
shaftTo: NSPoint(x: 350, y: 610),
|
||||
headLeft: NSPoint(x: 238, y: 490),
|
||||
headTip: NSPoint(x: 350, y: 640),
|
||||
headRight: NSPoint(x: 462, y: 490),
|
||||
tint: color(0xF4FFFC)
|
||||
)
|
||||
|
||||
// Down arrow on the right.
|
||||
strokeArrow(
|
||||
shaftFrom: NSPoint(x: 674, y: 736),
|
||||
shaftTo: NSPoint(x: 674, y: 414),
|
||||
headLeft: NSPoint(x: 562, y: 534),
|
||||
headTip: NSPoint(x: 674, y: 384),
|
||||
headRight: NSPoint(x: 786, y: 534),
|
||||
tint: color(0x5EEAD4)
|
||||
)
|
||||
|
||||
// Three small dots suggest a swarm: peers nearby, not a central server.
|
||||
for (point, alpha) in [
|
||||
(NSPoint(x: 250, y: 790), CGFloat(0.55)),
|
||||
(NSPoint(x: 512, y: 840), CGFloat(0.35)),
|
||||
(NSPoint(x: 790, y: 260), CGFloat(0.45)),
|
||||
] {
|
||||
let dot = NSBezierPath(ovalIn: NSRect(x: point.x - 7, y: point.y - 7, width: 14, height: 14))
|
||||
color(0xFFFFFF, alpha).setFill()
|
||||
dot.fill()
|
||||
}
|
||||
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
guard let cgImage = rep.cgImage else {
|
||||
fatalError("Could not make a CGImage")
|
||||
}
|
||||
|
||||
// App Store icons must not contain an alpha channel. Drawing the rendered
|
||||
// image into an opaque context and then encoding that image gives us an
|
||||
// RGB PNG without changing a single visible pixel.
|
||||
let width = cgImage.width
|
||||
let height = cgImage.height
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
guard let context = CGContext(
|
||||
data: nil,
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: 0,
|
||||
space: colorSpace,
|
||||
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue
|
||||
) else {
|
||||
fatalError("Could not create an opaque context")
|
||||
}
|
||||
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
||||
guard let opaque = context.makeImage() else {
|
||||
fatalError("Could not render the opaque icon")
|
||||
}
|
||||
|
||||
let url = URL(fileURLWithPath: output)
|
||||
try FileManager.default.createDirectory(
|
||||
at: url.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
guard let destination = CGImageDestinationCreateWithURL(
|
||||
url as CFURL,
|
||||
"public.png" as CFString,
|
||||
1,
|
||||
nil
|
||||
) else {
|
||||
fatalError("Could not create a PNG destination")
|
||||
}
|
||||
CGImageDestinationAddImage(destination, opaque, nil)
|
||||
guard CGImageDestinationFinalize(destination) else {
|
||||
fatalError("Could not write the PNG")
|
||||
}
|
||||
print("Wrote \(url.path)")
|
||||
Reference in New Issue
Block a user