Skip to content

Instantly share code, notes, and snippets.

@ethanhuang13
Last active February 7, 2024 05:58
Show Gist options
  • Star 63 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ethanhuang13/8587d10689e3735354f975f6a25ef9fa to your computer and use it in GitHub Desktop.
Save ethanhuang13/8587d10689e3735354f975f6a25ef9fa to your computer and use it in GitHub Desktop.
MacBook Air M1 Zhuyin Keyboard written with SwiftUI ~=300 LOC, < 4hrs
//
// VirtualKeyboard.swift
// MacBook Air M1 Zhuyin Keyboard
// Written with SwiftUI ~=300 LOC, < 4hrs
// Created by Ethan Huang on 2021/1/13.
// Twitter: @ethanhuang13
import SwiftUI
struct VirtualKeyboard: View {
var body: some View {
VStack(spacing: .zero) {
HStack(spacing: .zero) {
speaker
keyboard
.padding(.horizontal, 15)
speaker
}
.padding(.horizontal, 15)
.padding(.vertical, 15)
trackPad
}
.padding(.vertical)
.background(Color(white: 0.6))
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
}
private var speaker: some View {
Color(white: 0.5)
.frame(width: defaultWidth * 0.6)
.padding(.vertical)
}
private var trackPad: some View {
HStack { // Touchpad
Color(white: 0.5)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.frame(width: defaultWidth * 8.8, height: defaultHeight * 5)
}
}
private var keyboard: some View {
VStack(spacing: defaultWidth / 10) {
let allKeys: [[KeyCap.Config]] = [
[.esc,
.fn(top: "􀆫", bottom: "F1"),
.fn(top: "􀆭", bottom: "F2"),
.fn(top: "􀇴", bottom: "F3"),
.fn(top: "􀊫", bottom: "F4"),
.fn(top: "􀊰", bottom: "F5"),
.fn(top: "􀆹", bottom: "F6"),
.fn(top: "􀊉", bottom: "F7"),
.fn(top: "􀊇", bottom: "F8"),
.fn(top: "􀊋", bottom: "F9"),
.fn(top: "􀊠", bottom: "F10"),
.fn(top: "􀊤", bottom: "F11"),
.fn(top: "􀊨", bottom: "F12"),
.touchId],
[.topBottom(top: "~", bottom: "."),
.grid(["!", "1", "ㄅ", ""]),
.grid(["@", "2", "ㄉ", ""]),
.grid(["#", "3", "ˇ", ""]),
.grid(["$", "4", "ˋ", ""]),
.grid(["%", "5", "ㄓ", ""]),
.grid(["^", "6", "ˊ", ""]),
.grid(["&", "7", "˙", ""]),
.grid(["*", "8", "ㄚ", ""]),
.grid(["(", "9", "ㄞ", ""]),
.grid([")", "0", "ㄢ", ""]),
.grid(["_", "-", "ㄦ", ""]),
.topBottom(top: "+", bottom: "="),
.backspace],
[.tab,
.grid(["", "Q", "ㄆ", ""]),
.grid(["", "W", "ㄊ", ""]),
.grid(["", "E", "ㄍ", ""]),
.grid(["", "R", "ㄐ", ""]),
.grid(["", "T", "ㄔ", ""]),
.grid(["", "Y", "ㄗ", ""]),
.grid(["", "U", "ㄧ", ""]),
.grid(["", "I", "ㄛ", ""]),
.grid(["", "O", "ㄟ", ""]),
.grid(["", "P", "ㄣ", ""]),
.grid(["『", "{", "「", "["]),
.grid(["』", "}", "」", "]"]),
.grid(["", "|", "、", "\\"])],
[.capsLock,
.grid(["", "A", "ㄇ", ""]),
.grid(["", "S", "ㄋ", ""]),
.grid(["", "D", "ㄎ", ""]),
.grid(["", "F", "ㄑ", ""]),
.grid(["", "G", "ㄕ", ""]),
.grid(["", "H", "ㄘ", ""]),
.grid(["", "J", "ㄨ", ""]),
.grid(["", "K", "ㄜ", ""]),
.grid(["", "L", "ㄠ", ""]),
.grid(["", ":", "ㄤ", ";"]),
.topBottom(top: "\"", bottom: "'"),
.return],
[.leftShift,
.grid(["", "Z", "ㄈ", ""]),
.grid(["", "X", "ㄌ", ""]),
.grid(["", "C", "ㄏ", ""]),
.grid(["", "V", "ㄒ", ""]),
.grid(["", "B", "ㄖ", ""]),
.grid(["", "N", "ㄙ", ""]),
.grid(["", "M", "ㄩ", ""]),
.grid([",", "<", "ㄝ", ","]),
.grid(["。", ">", "ㄡ", "."]),
.grid(["?", "", "ㄥ", "/"]),
.rightShift],
[.globe,
.control,
.leftOption,
.leftCommand,
.space,
.rightCommand,
.rightOption,
.directions]
]
ForEach(0 ..< allKeys.count) { index in
let keys = allKeys[index]
HStack {
ForEach(keys.map { AnyIdentifiable($0) }) {
KeyCap($0.value)
}
}
}
}
.padding(10)
.background(Color(white: 0.5))
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
private let fnHeight: CGFloat = 25
private let defaultWidth: CGFloat = 50
private let defaultHeight: CGFloat = 50
struct AnyIdentifiable<T>: Identifiable {
let id = UUID()
let value: T
init(_ value: T) {
self.value = value
}
}
struct KeyCap: View {
enum Config {
case esc
case fn(top: String, bottom: String)
case touchId
case topBottom(top: String, bottom: String)
case grid([String])
case backspace
case tab
case capsLock, `return`
case leftShift, rightShift
case globe, control, leftOption, leftCommand, space, rightCommand, rightOption
case directions
}
init(_ config: Config) {
self.config = config
}
let config: Config
var body: some View {
if case Config.directions = config {
key
} else {
key
.padding(5)
.background(Color.black)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
}
private struct Grid: View {
let keys: [String]
var body: some View {
let columns = [GridItem(.fixed(defaultWidth / 2), spacing: 5),
GridItem(.fixed(defaultWidth / 2), spacing: 5)]
LazyVGrid(columns: columns, spacing: 0, content: {
ForEach(keys.map { AnyIdentifiable($0) }) {
Text($0.value)
.padding(0)
}
})
}
}
private struct TopBottom: View {
init(top: String, bottom: String, alignment: HorizontalAlignment = .center) {
self.top = top
self.bottom = bottom
self.alignment = alignment
}
let top: String
let bottom: String
let alignment: HorizontalAlignment
var body: some View {
VStack(alignment: alignment, spacing: 5) {
Text(top)
.padding(.horizontal, 3)
Text(bottom)
.padding(.horizontal, 3)
}
}
}
private struct Arrow: View {
let key: String
var body: some View {
Text(key)
.font(.caption)
.frame(width: defaultWidth * 1.15, height: defaultHeight / 1.7)
.background(Color.black)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
}
@ViewBuilder
var key: some View {
switch config {
case .esc:
HStack {
Text("esc")
.frame(width: defaultWidth, height: fnHeight, alignment: .leading)
}
case .fn(let top, let bottom):
VStack {
Text(top)
Text(bottom)
.font(.caption2)
}
.frame(width: defaultWidth * 1.083, height: fnHeight)
case .touchId:
Text("")
.frame(width: defaultWidth / 2, height: fnHeight)
case .topBottom(let top, let bottom):
TopBottom(top: top, bottom: bottom)
.frame(width: defaultWidth, height: defaultHeight)
.font(.title2)
.frame(width: defaultWidth, height: defaultHeight)
case .grid(let keys):
Grid(keys: keys)
.font(.title2)
.frame(width: defaultWidth, height: defaultHeight)
case .backspace:
TopBottom(top: "", bottom: "􀆛")
.frame(width: defaultWidth * 1.5, height: defaultHeight, alignment: .trailing)
case .tab:
TopBottom(top: "", bottom: "􀅆")
.frame(width: defaultWidth * 1.5, height: defaultHeight, alignment: .leading)
case .capsLock:
TopBottom(top: "•", bottom: "中/英", alignment: .leading)
.frame(width: defaultWidth * 2, height: defaultHeight, alignment: .leading)
case .return:
TopBottom(top: "", bottom: "􀅇")
.frame(width: defaultWidth * 1.9, height: defaultHeight, alignment: .trailing)
case .leftShift:
TopBottom(top: "", bottom: "􀆝")
.frame(width: defaultWidth * 2.6, height: defaultHeight, alignment: .leading)
case .rightShift:
TopBottom(top: "", bottom: "􀆝")
.frame(width: defaultWidth * 2.6, height: defaultHeight, alignment: .trailing)
case .globe:
Grid(keys: ["", "fn", "􀆪", ""])
.font(.title3)
.frame(width: defaultWidth, height: defaultHeight)
case .control:
TopBottom(top: "􀆍", bottom: "control", alignment: .trailing)
.frame(width: defaultWidth, height: defaultHeight, alignment: .trailing)
case .leftOption:
TopBottom(top: "􀆕", bottom: "option", alignment: .trailing)
.frame(width: defaultWidth, height: defaultHeight, alignment: .trailing)
case .leftCommand:
TopBottom(top: "􀆔", bottom: "command", alignment: .trailing)
.frame(width: defaultWidth * 1.35, height: defaultHeight, alignment: .trailing)
case .space:
Color.clear
.frame(width: defaultWidth * 6.4, height: defaultHeight)
case .rightCommand:
TopBottom(top: "􀆔", bottom: "command", alignment: .trailing)
.frame(width: defaultWidth * 1.35, height: defaultHeight, alignment: .leading)
case .rightOption:
TopBottom(top: "􀆕", bottom: "option", alignment: .trailing)
.frame(width: defaultWidth, height: defaultHeight, alignment: .leading)
case .directions:
VStack(spacing: 0) {
HStack {
Arrow(key: "􀄤")
}
HStack(spacing: 8) {
Arrow(key: "􀄦")
Arrow(key: "􀄥")
Arrow(key: "􀄧")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
VirtualKeyboard()
}
}
@lidaobing
Copy link

How about add a screenshot?

@ethanhuang13
Copy link
Author

Screenshot

CleanShot 2021-01-15 at 13 46 31@2x

@ethanhuang13
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment